mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'canary' into 1345-domain-not-working-after-server-restart-or-traefik-reload
This commit is contained in:
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Create a bug report
|
description: Create a bug report
|
||||||
labels: ["bug"]
|
labels: ["needs-triage🔍"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@@ -62,6 +62,7 @@ body:
|
|||||||
- "Docker"
|
- "Docker"
|
||||||
- "Remote server"
|
- "Remote server"
|
||||||
- "Local Development"
|
- "Local Development"
|
||||||
|
- "Cloud Version"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
@@ -138,11 +138,18 @@ curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
|||||||
&& ./install.sh
|
&& ./install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Railpack
|
||||||
|
curl -sSL https://railpack.com/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install Buildpacks
|
# Install Buildpacks
|
||||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Pull Request
|
## Pull Request
|
||||||
|
|
||||||
- The `main` branch is the source of truth and should always reflect the latest stable release.
|
- The `main` branch is the source of truth and should always reflect the latest stable release.
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
|||||||
&& ./install.sh \
|
&& ./install.sh \
|
||||||
&& pnpm install -g tsx
|
&& pnpm install -g tsx
|
||||||
|
|
||||||
|
# Install Railpack
|
||||||
|
ARG RAILPACK_VERSION=0.0.37
|
||||||
|
RUN curl -sSL https://railpack.com/install.sh | bash
|
||||||
|
|
||||||
# Install buildpacks
|
# Install buildpacks
|
||||||
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ describe("createDomainLabels", () => {
|
|||||||
port: 8080,
|
port: 8080,
|
||||||
https: false,
|
https: false,
|
||||||
uniqueConfigKey: 1,
|
uniqueConfigKey: 1,
|
||||||
|
customCertResolver: null,
|
||||||
certificateType: "none",
|
certificateType: "none",
|
||||||
applicationId: "",
|
applicationId: "",
|
||||||
composeId: "",
|
composeId: "",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ if (typeof window === "undefined") {
|
|||||||
const baseApp: ApplicationNested = {
|
const baseApp: ApplicationNested = {
|
||||||
applicationId: "",
|
applicationId: "",
|
||||||
herokuVersion: "",
|
herokuVersion: "",
|
||||||
|
watchPaths: [],
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
appName: "",
|
appName: "",
|
||||||
autoDeploy: true,
|
autoDeploy: true,
|
||||||
@@ -37,6 +38,7 @@ const baseApp: ApplicationNested = {
|
|||||||
isPreviewDeploymentsActive: false,
|
isPreviewDeploymentsActive: false,
|
||||||
previewBuildArgs: null,
|
previewBuildArgs: null,
|
||||||
previewCertificateType: "none",
|
previewCertificateType: "none",
|
||||||
|
previewCustomCertResolver: null,
|
||||||
previewEnv: null,
|
previewEnv: null,
|
||||||
previewHttps: false,
|
previewHttps: false,
|
||||||
previewPath: "/",
|
previewPath: "/",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const baseAdmin: User = {
|
|||||||
letsEncryptEmail: null,
|
letsEncryptEmail: null,
|
||||||
sshPrivateKey: null,
|
sshPrivateKey: null,
|
||||||
enableDockerCleanup: false,
|
enableDockerCleanup: false,
|
||||||
enableLogRotation: false,
|
logCleanupCron: null,
|
||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
stripeCustomerId: "",
|
stripeCustomerId: "",
|
||||||
stripeSubscriptionId: "",
|
stripeSubscriptionId: "",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const baseApp: ApplicationNested = {
|
|||||||
branch: null,
|
branch: null,
|
||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
registryUrl: "",
|
registryUrl: "",
|
||||||
|
watchPaths: [],
|
||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
isPreviewDeploymentsActive: false,
|
isPreviewDeploymentsActive: false,
|
||||||
previewBuildArgs: null,
|
previewBuildArgs: null,
|
||||||
@@ -23,6 +24,7 @@ const baseApp: ApplicationNested = {
|
|||||||
previewPath: "/",
|
previewPath: "/",
|
||||||
previewPort: 3000,
|
previewPort: 3000,
|
||||||
previewLimit: 0,
|
previewLimit: 0,
|
||||||
|
previewCustomCertResolver: null,
|
||||||
previewWildcard: "",
|
previewWildcard: "",
|
||||||
project: {
|
project: {
|
||||||
env: "",
|
env: "",
|
||||||
@@ -103,6 +105,7 @@ const baseDomain: Domain = {
|
|||||||
port: null,
|
port: null,
|
||||||
serviceName: "",
|
serviceName: "",
|
||||||
composeId: "",
|
composeId: "",
|
||||||
|
customCertResolver: null,
|
||||||
domainType: "application",
|
domainType: "application",
|
||||||
uniqueConfigKey: 1,
|
uniqueConfigKey: 1,
|
||||||
previewDeploymentId: "",
|
previewDeploymentId: "",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +26,7 @@ enum BuildType {
|
|||||||
paketo_buildpacks = "paketo_buildpacks",
|
paketo_buildpacks = "paketo_buildpacks",
|
||||||
nixpacks = "nixpacks",
|
nixpacks = "nixpacks",
|
||||||
static = "static",
|
static = "static",
|
||||||
|
railpack = "railpack",
|
||||||
}
|
}
|
||||||
|
|
||||||
const mySchema = z.discriminatedUnion("buildType", [
|
const mySchema = z.discriminatedUnion("buildType", [
|
||||||
@@ -53,6 +55,9 @@ const mySchema = z.discriminatedUnion("buildType", [
|
|||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal("static"),
|
buildType: z.literal("static"),
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
buildType: z.literal("railpack"),
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type AddTemplate = z.infer<typeof mySchema>;
|
type AddTemplate = z.infer<typeof mySchema>;
|
||||||
@@ -173,6 +178,15 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
Dockerfile
|
Dockerfile
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroupItem value="railpack" />
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="font-normal">
|
||||||
|
Railpack{" "}
|
||||||
|
<Badge className="ml-1 text-xs px-1">New</Badge>
|
||||||
|
</FormLabel>
|
||||||
|
</FormItem>
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RadioGroupItem value="nixpacks" />
|
<RadioGroupItem value="nixpacks" />
|
||||||
|
|||||||
@@ -85,8 +85,20 @@ export const AddDomain = ({
|
|||||||
|
|
||||||
const form = useForm<Domain>({
|
const form = useForm<Domain>({
|
||||||
resolver: zodResolver(domain),
|
resolver: zodResolver(domain),
|
||||||
|
defaultValues: {
|
||||||
|
host: "",
|
||||||
|
path: undefined,
|
||||||
|
port: undefined,
|
||||||
|
https: false,
|
||||||
|
certificateType: undefined,
|
||||||
|
customCertResolver: undefined,
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const certificateType = form.watch("certificateType");
|
||||||
|
const https = form.watch("https");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
@@ -94,13 +106,29 @@ export const AddDomain = ({
|
|||||||
/* Convert null to undefined */
|
/* Convert null to undefined */
|
||||||
path: data?.path || undefined,
|
path: data?.path || undefined,
|
||||||
port: data?.port || undefined,
|
port: data?.port || undefined,
|
||||||
|
certificateType: data?.certificateType || undefined,
|
||||||
|
customCertResolver: data?.customCertResolver || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!domainId) {
|
if (!domainId) {
|
||||||
form.reset({});
|
form.reset({
|
||||||
|
host: "",
|
||||||
|
path: undefined,
|
||||||
|
port: undefined,
|
||||||
|
https: false,
|
||||||
|
certificateType: undefined,
|
||||||
|
customCertResolver: undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data, isLoading]);
|
}, [form, data, isLoading, domainId]);
|
||||||
|
|
||||||
|
// Separate effect for handling custom cert resolver validation
|
||||||
|
useEffect(() => {
|
||||||
|
if (certificateType === "custom") {
|
||||||
|
form.trigger("customCertResolver");
|
||||||
|
}
|
||||||
|
}, [certificateType, form]);
|
||||||
|
|
||||||
const dictionary = {
|
const dictionary = {
|
||||||
success: domainId ? "Domain Updated" : "Domain Created",
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
@@ -256,34 +284,73 @@ export const AddDomain = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{form.getValues().https && (
|
{https && (
|
||||||
<FormField
|
<>
|
||||||
control={form.control}
|
<FormField
|
||||||
name="certificateType"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="certificateType"
|
||||||
<FormItem className="col-span-2">
|
render={({ field }) => {
|
||||||
<FormLabel>Certificate Provider</FormLabel>
|
return (
|
||||||
<Select
|
<FormItem>
|
||||||
onValueChange={field.onChange}
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
defaultValue={field.value || ""}
|
<Select
|
||||||
>
|
onValueChange={(value) => {
|
||||||
<FormControl>
|
field.onChange(value);
|
||||||
<SelectTrigger>
|
if (value !== "custom") {
|
||||||
<SelectValue placeholder="Select a certificate provider" />
|
form.setValue(
|
||||||
</SelectTrigger>
|
"customCertResolver",
|
||||||
</FormControl>
|
undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a certificate provider" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={"none"}>None</SelectItem>
|
||||||
|
<SelectItem value={"letsencrypt"}>
|
||||||
|
Let's Encrypt
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={"custom"}>Custom</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<SelectContent>
|
{certificateType === "custom" && (
|
||||||
<SelectItem value="none">None</SelectItem>
|
<FormField
|
||||||
<SelectItem value={"letsencrypt"}>
|
control={form.control}
|
||||||
Let's Encrypt
|
name="customCertResolver"
|
||||||
</SelectItem>
|
render={({ field }) => {
|
||||||
</SelectContent>
|
return (
|
||||||
</Select>
|
<FormItem>
|
||||||
<FormMessage />
|
<FormLabel>Custom Certificate Resolver</FormLabel>
|
||||||
</FormItem>
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Enter your custom certificate resolver"
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
field.onChange(e);
|
||||||
|
form.trigger("customCertResolver");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,15 +71,19 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
|||||||
resolver: zodResolver(addEnvironmentSchema),
|
resolver: zodResolver(addEnvironmentSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Watch form value
|
||||||
|
const currentEnvironment = form.watch("environment");
|
||||||
|
const hasChanges = currentEnvironment !== (data?.env || "");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
environment: data.env || "",
|
environment: data.env || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [data, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: EnvironmentSchema) => {
|
const onSubmit = async (formData: EnvironmentSchema) => {
|
||||||
mutateAsync({
|
mutateAsync({
|
||||||
mongoId: id || "",
|
mongoId: id || "",
|
||||||
postgresId: id || "",
|
postgresId: id || "",
|
||||||
@@ -87,7 +91,7 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
|||||||
mysqlId: id || "",
|
mysqlId: id || "",
|
||||||
mariadbId: id || "",
|
mariadbId: id || "",
|
||||||
composeId: id || "",
|
composeId: id || "",
|
||||||
env: data.environment,
|
env: formData.environment,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Environments Added");
|
toast.success("Environments Added");
|
||||||
@@ -98,6 +102,12 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
form.reset({
|
||||||
|
environment: data?.env || "",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
@@ -106,6 +116,11 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
|||||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
You can add environment variables to your resource.
|
You can add environment variables to your resource.
|
||||||
|
{hasChanges && (
|
||||||
|
<span className="text-yellow-500 ml-2">
|
||||||
|
(You have unsaved changes)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -132,8 +147,8 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="environment"
|
name="environment"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="w-full">
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl className="">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
@@ -142,21 +157,35 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
|||||||
}
|
}
|
||||||
language="properties"
|
language="properties"
|
||||||
disabled={isEnvVisible}
|
disabled={isEnvVisible}
|
||||||
|
className="font-mono"
|
||||||
|
wrapperClassName="compose-file-editor"
|
||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
`}
|
`}
|
||||||
className="h-96 font-mono"
|
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end">
|
<div className="flex flex-row justify-end gap-2">
|
||||||
<Button isLoading={isLoading} className="w-fit" type="submit">
|
{hasChanges && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="w-fit"
|
||||||
|
type="submit"
|
||||||
|
disabled={!hasChanges}
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
const addEnvironmentSchema = z.object({
|
const addEnvironmentSchema = z.object({
|
||||||
env: z.string(),
|
env: z.string(),
|
||||||
@@ -34,16 +35,32 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
|
|
||||||
const form = useForm<EnvironmentSchema>({
|
const form = useForm<EnvironmentSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
env: data?.env || "",
|
env: "",
|
||||||
buildArgs: data?.buildArgs || "",
|
buildArgs: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addEnvironmentSchema),
|
resolver: zodResolver(addEnvironmentSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async (data: EnvironmentSchema) => {
|
// Watch form values
|
||||||
|
const currentEnv = form.watch("env");
|
||||||
|
const currentBuildArgs = form.watch("buildArgs");
|
||||||
|
const hasChanges =
|
||||||
|
currentEnv !== (data?.env || "") ||
|
||||||
|
currentBuildArgs !== (data?.buildArgs || "");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
env: data.env || "",
|
||||||
|
buildArgs: data.buildArgs || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: EnvironmentSchema) => {
|
||||||
mutateAsync({
|
mutateAsync({
|
||||||
env: data.env,
|
env: formData.env,
|
||||||
buildArgs: data.buildArgs,
|
buildArgs: formData.buildArgs,
|
||||||
applicationId,
|
applicationId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
@@ -55,6 +72,13 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
form.reset({
|
||||||
|
env: data?.env || "",
|
||||||
|
buildArgs: data?.buildArgs || "",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background px-6 pb-6">
|
<Card className="bg-background px-6 pb-6">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -65,7 +89,16 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
<Secrets
|
<Secrets
|
||||||
name="env"
|
name="env"
|
||||||
title="Environment Settings"
|
title="Environment Settings"
|
||||||
description="You can add environment variables to your resource."
|
description={
|
||||||
|
<span>
|
||||||
|
You can add environment variables to your resource.
|
||||||
|
{hasChanges && (
|
||||||
|
<span className="text-yellow-500 ml-2">
|
||||||
|
(You have unsaved changes)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
placeholder={["NODE_ENV=production", "PORT=3000"].join("\n")}
|
placeholder={["NODE_ENV=production", "PORT=3000"].join("\n")}
|
||||||
/>
|
/>
|
||||||
{data?.buildType === "dockerfile" && (
|
{data?.buildType === "dockerfile" && (
|
||||||
@@ -89,8 +122,18 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
placeholder="NPM_TOKEN=xyz"
|
placeholder="NPM_TOKEN=xyz"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-row justify-end">
|
<div className="flex flex-row justify-end gap-2">
|
||||||
<Button isLoading={isLoading} className="w-fit" type="submit">
|
{hasChanges && (
|
||||||
|
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="w-fit"
|
||||||
|
type="submit"
|
||||||
|
disabled={!hasChanges}
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,14 +29,23 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
const BitbucketProviderSchema = z.object({
|
const BitbucketProviderSchema = z.object({
|
||||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
@@ -48,6 +57,7 @@ const BitbucketProviderSchema = z.object({
|
|||||||
.required(),
|
.required(),
|
||||||
branch: z.string().min(1, "Branch is required"),
|
branch: z.string().min(1, "Branch is required"),
|
||||||
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
||||||
|
watchPaths: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
|
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
|
||||||
@@ -73,6 +83,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
},
|
},
|
||||||
bitbucketId: "",
|
bitbucketId: "",
|
||||||
branch: "",
|
branch: "",
|
||||||
|
watchPaths: [],
|
||||||
},
|
},
|
||||||
resolver: zodResolver(BitbucketProviderSchema),
|
resolver: zodResolver(BitbucketProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -118,6 +129,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
},
|
},
|
||||||
buildPath: data.bitbucketBuildPath || "/",
|
buildPath: data.bitbucketBuildPath || "/",
|
||||||
bitbucketId: data.bitbucketId || "",
|
bitbucketId: data.bitbucketId || "",
|
||||||
|
watchPaths: data.watchPaths || [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -130,6 +142,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
bitbucketBuildPath: data.buildPath,
|
bitbucketBuildPath: data.buildPath,
|
||||||
bitbucketId: data.bitbucketId,
|
bitbucketId: data.bitbucketId,
|
||||||
applicationId,
|
applicationId,
|
||||||
|
watchPaths: data.watchPaths || [],
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provided Saved");
|
||||||
@@ -195,7 +208,20 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
name="repository"
|
name="repository"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="md:col-span-2 flex flex-col">
|
<FormItem className="md:col-span-2 flex flex-col">
|
||||||
<FormLabel>Repository</FormLabel>
|
<div className="flex items-center justify-between">
|
||||||
|
<FormLabel>Repository</FormLabel>
|
||||||
|
{field.value.owner && field.value.repo && (
|
||||||
|
<Link
|
||||||
|
href={`https://bitbucket.org/${field.value.owner}/${field.value.repo}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
|
>
|
||||||
|
<BitbucketIcon className="h-4 w-4" />
|
||||||
|
<span>View Repository</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -363,6 +389,84 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="watchPaths"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||||
|
?
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Add paths to watch for changes. When files in these
|
||||||
|
paths change, a new deployment will be triggered.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{field.value?.map((path, index) => (
|
||||||
|
<Badge key={index} variant="secondary">
|
||||||
|
{path}
|
||||||
|
<X
|
||||||
|
className="ml-1 size-3 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
const newPaths = [...(field.value || [])];
|
||||||
|
newPaths.splice(index, 1);
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.currentTarget;
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
const newPaths = [...(field.value || []), value];
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.querySelector(
|
||||||
|
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
const newPaths = [...(field.value || []), value];
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -17,23 +17,33 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { KeyRoundIcon, LockIcon } from "lucide-react";
|
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
|
||||||
const GitProviderSchema = z.object({
|
const GitProviderSchema = z.object({
|
||||||
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
repositoryURL: z.string().min(1, {
|
repositoryURL: z.string().min(1, {
|
||||||
message: "Repository URL is required",
|
message: "Repository URL is required",
|
||||||
}),
|
}),
|
||||||
branch: z.string().min(1, "Branch required"),
|
branch: z.string().min(1, "Branch required"),
|
||||||
buildPath: z.string().min(1, "Build Path required"),
|
|
||||||
sshKey: z.string().optional(),
|
sshKey: z.string().optional(),
|
||||||
|
watchPaths: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GitProvider = z.infer<typeof GitProviderSchema>;
|
type GitProvider = z.infer<typeof GitProviderSchema>;
|
||||||
@@ -56,6 +66,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
buildPath: "/",
|
buildPath: "/",
|
||||||
repositoryURL: "",
|
repositoryURL: "",
|
||||||
sshKey: undefined,
|
sshKey: undefined,
|
||||||
|
watchPaths: [],
|
||||||
},
|
},
|
||||||
resolver: zodResolver(GitProviderSchema),
|
resolver: zodResolver(GitProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -67,6 +78,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
branch: data.customGitBranch || "",
|
branch: data.customGitBranch || "",
|
||||||
buildPath: data.customGitBuildPath || "/",
|
buildPath: data.customGitBuildPath || "/",
|
||||||
repositoryURL: data.customGitUrl || "",
|
repositoryURL: data.customGitUrl || "",
|
||||||
|
watchPaths: data.watchPaths || [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -78,6 +90,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
customGitUrl: values.repositoryURL,
|
customGitUrl: values.repositoryURL,
|
||||||
customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey,
|
customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey,
|
||||||
applicationId,
|
applicationId,
|
||||||
|
watchPaths: values.watchPaths || [],
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Git Provider Saved");
|
toast.success("Git Provider Saved");
|
||||||
@@ -102,9 +115,22 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
name="repositoryURL"
|
name="repositoryURL"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Repository URL</FormLabel>
|
<div className="flex items-center justify-between">
|
||||||
|
<FormLabel>Repository URL</FormLabel>
|
||||||
|
{field.value?.startsWith("https://") && (
|
||||||
|
<Link
|
||||||
|
href={field.value}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
|
>
|
||||||
|
<GitIcon className="h-4 w-4" />
|
||||||
|
<span>View Repository</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="git@bitbucket.org" {...field} />
|
<Input placeholder="Repository URL" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -160,19 +186,22 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<FormField
|
<div className="space-y-4">
|
||||||
control={form.control}
|
<FormField
|
||||||
name="branch"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="branch"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>Branch</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>Branch</FormLabel>
|
||||||
<Input placeholder="Branch" {...field} />
|
<FormControl>
|
||||||
</FormControl>
|
<Input placeholder="Branch" {...field} />
|
||||||
<FormMessage />
|
</FormControl>
|
||||||
</FormItem>
|
<FormMessage />
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="buildPath"
|
name="buildPath"
|
||||||
@@ -186,6 +215,85 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="watchPaths"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||||
|
?
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-[300px]">
|
||||||
|
<p>
|
||||||
|
Add paths to watch for changes. When files in these
|
||||||
|
paths change, a new deployment will be triggered. This
|
||||||
|
will work only when manual webhook is setup.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{field.value?.map((path, index) => (
|
||||||
|
<Badge key={index} variant="secondary">
|
||||||
|
{path}
|
||||||
|
<X
|
||||||
|
className="ml-1 size-3 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
const newPaths = [...(field.value || [])];
|
||||||
|
newPaths.splice(index, 1);
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.currentTarget;
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
const newPaths = [...(field.value || []), value];
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.querySelector(
|
||||||
|
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
const newPaths = [...(field.value || []), value];
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end">
|
<div className="flex flex-row justify-end">
|
||||||
|
|||||||
@@ -28,14 +28,23 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
|
||||||
const GithubProviderSchema = z.object({
|
const GithubProviderSchema = z.object({
|
||||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
@@ -47,6 +56,7 @@ const GithubProviderSchema = z.object({
|
|||||||
.required(),
|
.required(),
|
||||||
branch: z.string().min(1, "Branch is required"),
|
branch: z.string().min(1, "Branch is required"),
|
||||||
githubId: z.string().min(1, "Github Provider is required"),
|
githubId: z.string().min(1, "Github Provider is required"),
|
||||||
|
watchPaths: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GithubProvider = z.infer<typeof GithubProviderSchema>;
|
type GithubProvider = z.infer<typeof GithubProviderSchema>;
|
||||||
@@ -113,6 +123,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
},
|
},
|
||||||
buildPath: data.buildPath || "/",
|
buildPath: data.buildPath || "/",
|
||||||
githubId: data.githubId || "",
|
githubId: data.githubId || "",
|
||||||
|
watchPaths: data.watchPaths || [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -125,6 +136,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
owner: data.repository.owner,
|
owner: data.repository.owner,
|
||||||
buildPath: data.buildPath,
|
buildPath: data.buildPath,
|
||||||
githubId: data.githubId,
|
githubId: data.githubId,
|
||||||
|
watchPaths: data.watchPaths || [],
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provided Saved");
|
||||||
@@ -187,7 +199,20 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
name="repository"
|
name="repository"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="md:col-span-2 flex flex-col">
|
<FormItem className="md:col-span-2 flex flex-col">
|
||||||
<FormLabel>Repository</FormLabel>
|
<div className="flex items-center justify-between">
|
||||||
|
<FormLabel>Repository</FormLabel>
|
||||||
|
{field.value.owner && field.value.repo && (
|
||||||
|
<Link
|
||||||
|
href={`https://github.com/${field.value.owner}/${field.value.repo}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
|
>
|
||||||
|
<GithubIcon className="h-4 w-4" />
|
||||||
|
<span>View Repository</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -350,7 +375,85 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="/" {...field} />
|
<Input placeholder="/" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="watchPaths"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Add paths to watch for changes. When files in these
|
||||||
|
paths change, a new deployment will be triggered.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{field.value?.map((path, index) => (
|
||||||
|
<Badge
|
||||||
|
key={index}
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{path}
|
||||||
|
<X
|
||||||
|
className="size-3 cursor-pointer hover:text-destructive"
|
||||||
|
onClick={() => {
|
||||||
|
const newPaths = [...(field.value || [])];
|
||||||
|
newPaths.splice(index, 1);
|
||||||
|
field.onChange(newPaths);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.currentTarget;
|
||||||
|
const path = input.value.trim();
|
||||||
|
if (path) {
|
||||||
|
field.onChange([...(field.value || []), path]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.querySelector(
|
||||||
|
'input[placeholder*="Enter a path"]',
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const path = input.value.trim();
|
||||||
|
if (path) {
|
||||||
|
field.onChange([...(field.value || []), path]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -365,6 +468,16 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{/* create github link */}
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
|
<Link
|
||||||
|
href={`https://github.com/${repository?.owner}/${repository?.repo}`}
|
||||||
|
target="_blank"
|
||||||
|
className="w-fit"
|
||||||
|
>
|
||||||
|
Repository
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,14 +29,23 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
|
||||||
const GitlabProviderSchema = z.object({
|
const GitlabProviderSchema = z.object({
|
||||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
@@ -50,6 +59,7 @@ const GitlabProviderSchema = z.object({
|
|||||||
.required(),
|
.required(),
|
||||||
branch: z.string().min(1, "Branch is required"),
|
branch: z.string().min(1, "Branch is required"),
|
||||||
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
||||||
|
watchPaths: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
|
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
|
||||||
@@ -124,6 +134,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
},
|
},
|
||||||
buildPath: data.gitlabBuildPath || "/",
|
buildPath: data.gitlabBuildPath || "/",
|
||||||
gitlabId: data.gitlabId || "",
|
gitlabId: data.gitlabId || "",
|
||||||
|
watchPaths: data.watchPaths || [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -138,6 +149,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
applicationId,
|
applicationId,
|
||||||
gitlabProjectId: data.repository.id,
|
gitlabProjectId: data.repository.id,
|
||||||
gitlabPathNamespace: data.repository.gitlabPathNamespace,
|
gitlabPathNamespace: data.repository.gitlabPathNamespace,
|
||||||
|
watchPaths: data.watchPaths || [],
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provided Saved");
|
||||||
@@ -203,7 +215,20 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
name="repository"
|
name="repository"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="md:col-span-2 flex flex-col">
|
<FormItem className="md:col-span-2 flex flex-col">
|
||||||
<FormLabel>Repository</FormLabel>
|
<div className="flex items-center justify-between">
|
||||||
|
<FormLabel>Repository</FormLabel>
|
||||||
|
{field.value.owner && field.value.repo && (
|
||||||
|
<Link
|
||||||
|
href={`https://gitlab.com/${field.value.owner}/${field.value.repo}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
|
>
|
||||||
|
<GitlabIcon className="h-4 w-4" />
|
||||||
|
<span>View Repository</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -375,7 +400,85 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="/" {...field} />
|
<Input placeholder="/" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="watchPaths"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Add paths to watch for changes. When files in these
|
||||||
|
paths change, a new deployment will be triggered.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{field.value?.map((path, index) => (
|
||||||
|
<Badge
|
||||||
|
key={index}
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{path}
|
||||||
|
<X
|
||||||
|
className="size-3 cursor-pointer hover:text-destructive"
|
||||||
|
onClick={() => {
|
||||||
|
const newPaths = [...(field.value || [])];
|
||||||
|
newPaths.splice(index, 1);
|
||||||
|
field.onChange(newPaths);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.currentTarget;
|
||||||
|
const path = input.value.trim();
|
||||||
|
if (path) {
|
||||||
|
field.onChange([...(field.value || []), path]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.querySelector(
|
||||||
|
'input[placeholder*="Enter a path"]',
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const path = input.value.trim();
|
||||||
|
if (path) {
|
||||||
|
field.onChange([...(field.value || []), path]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,8 +4,22 @@ import { DialogAction } from "@/components/shared/dialog-action";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, Hammer, RefreshCcw, Terminal } from "lucide-react";
|
import {
|
||||||
|
Ban,
|
||||||
|
CheckCircle2,
|
||||||
|
Hammer,
|
||||||
|
HelpCircle,
|
||||||
|
RefreshCcw,
|
||||||
|
Terminal,
|
||||||
|
} from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
@@ -41,128 +55,188 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<DialogAction
|
<TooltipProvider delayDuration={0}>
|
||||||
title="Deploy Application"
|
|
||||||
description="Are you sure you want to deploy this application?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await deploy({
|
|
||||||
applicationId: applicationId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Application deployed successfully");
|
|
||||||
refetch();
|
|
||||||
router.push(
|
|
||||||
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deploying application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Application"
|
|
||||||
description="Are you sure you want to reload this application?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
applicationId: applicationId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Application reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
|
||||||
Reload
|
|
||||||
<RefreshCcw className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Rebuild Application"
|
|
||||||
description="Are you sure you want to rebuild this application?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await redeploy({
|
|
||||||
applicationId: applicationId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Application rebuilt successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error rebuilding application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Rebuild
|
|
||||||
<Hammer className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Application"
|
title="Deploy Application"
|
||||||
description="Are you sure you want to start this application?"
|
description="Are you sure you want to deploy this application?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
await deploy({
|
||||||
applicationId: applicationId,
|
applicationId: applicationId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Application started successfully");
|
toast.success("Application deployed successfully");
|
||||||
refetch();
|
refetch();
|
||||||
|
router.push(
|
||||||
|
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error starting application");
|
toast.error("Error deploying application");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isStarting}>
|
<Button
|
||||||
Start
|
variant="default"
|
||||||
<CheckCircle2 className="size-4" />
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Downloads the source code and performs a complete build
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Application"
|
title="Reload Application"
|
||||||
description="Are you sure you want to stop this application?"
|
description="Are you sure you want to reload this application?"
|
||||||
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await reload({
|
||||||
applicationId: applicationId,
|
applicationId: applicationId,
|
||||||
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Application stopped successfully");
|
toast.success("Application reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error stopping application");
|
toast.error("Error reloading application");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="destructive" isLoading={isStopping}>
|
<Button variant="secondary" isLoading={isReloading}>
|
||||||
Stop
|
Reload
|
||||||
<Ban className="size-4" />
|
<RefreshCcw className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
<DialogAction
|
||||||
|
title="Rebuild Application"
|
||||||
|
description="Are you sure you want to rebuild this application?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await redeploy({
|
||||||
|
applicationId: applicationId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Application rebuilt successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error rebuilding application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Rebuild
|
||||||
|
<Hammer className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Only rebuilds the application without downloading new
|
||||||
|
code
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
|
||||||
|
{data?.applicationStatus === "idle" ? (
|
||||||
|
<DialogAction
|
||||||
|
title="Start Application"
|
||||||
|
description="Are you sure you want to start this application?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
applicationId: applicationId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Application started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the application (requires a previous successful
|
||||||
|
build)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Application"
|
||||||
|
description="Are you sure you want to stop this application?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
applicationId: applicationId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Application stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running application</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export const AddPreviewDomain = ({
|
|||||||
/* Convert null to undefined */
|
/* Convert null to undefined */
|
||||||
path: data?.path || undefined,
|
path: data?.path || undefined,
|
||||||
port: data?.port || undefined,
|
port: data?.port || undefined,
|
||||||
|
customCertResolver: data?.customCertResolver || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,16 +35,30 @@ import { useForm } from "react-hook-form";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z
|
||||||
env: z.string(),
|
.object({
|
||||||
buildArgs: z.string(),
|
env: z.string(),
|
||||||
wildcardDomain: z.string(),
|
buildArgs: z.string(),
|
||||||
port: z.number(),
|
wildcardDomain: z.string(),
|
||||||
previewLimit: z.number(),
|
port: z.number(),
|
||||||
previewHttps: z.boolean(),
|
previewLimit: z.number(),
|
||||||
previewPath: z.string(),
|
previewHttps: z.boolean(),
|
||||||
previewCertificateType: z.enum(["letsencrypt", "none"]),
|
previewPath: z.string(),
|
||||||
});
|
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||||
|
previewCustomCertResolver: z.string().optional(),
|
||||||
|
})
|
||||||
|
.superRefine((input, ctx) => {
|
||||||
|
if (
|
||||||
|
input.previewCertificateType === "custom" &&
|
||||||
|
!input.previewCustomCertResolver
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["previewCustomCertResolver"],
|
||||||
|
message: "Required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
type Schema = z.infer<typeof schema>;
|
type Schema = z.infer<typeof schema>;
|
||||||
|
|
||||||
@@ -90,6 +104,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
previewHttps: data.previewHttps || false,
|
previewHttps: data.previewHttps || false,
|
||||||
previewPath: data.previewPath || "/",
|
previewPath: data.previewPath || "/",
|
||||||
previewCertificateType: data.previewCertificateType || "none",
|
previewCertificateType: data.previewCertificateType || "none",
|
||||||
|
previewCustomCertResolver: data.previewCustomCertResolver || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
@@ -105,6 +120,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
previewHttps: formData.previewHttps,
|
previewHttps: formData.previewHttps,
|
||||||
previewPath: formData.previewPath,
|
previewPath: formData.previewPath,
|
||||||
previewCertificateType: formData.previewCertificateType,
|
previewCertificateType: formData.previewCertificateType,
|
||||||
|
previewCustomCertResolver: formData.previewCustomCertResolver,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Preview Deployments settings updated");
|
toast.success("Preview Deployments settings updated");
|
||||||
@@ -184,10 +200,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Preview Limit</FormLabel>
|
<FormLabel>Preview Limit</FormLabel>
|
||||||
{/* <FormDescription>
|
|
||||||
Set the limit of preview deployments that can be
|
|
||||||
created for this app.
|
|
||||||
</FormDescription> */}
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<NumberInput placeholder="3000" {...field} />
|
<NumberInput placeholder="3000" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -238,6 +250,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Let's Encrypt
|
Let's Encrypt
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
<SelectItem value={"custom"}>Custom</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -245,6 +258,25 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{form.watch("previewCertificateType") === "custom" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="previewCustomCertResolver"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="my-custom-resolver"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 lg:grid-cols-2">
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
<div className="flex flex-row items-center justify-between rounded-lg border p-4 col-span-2">
|
<div className="flex flex-row items-center justify-between rounded-lg border p-4 col-span-2">
|
||||||
|
|||||||
@@ -104,6 +104,15 @@ export const AddDomainCompose = ({
|
|||||||
|
|
||||||
const form = useForm<Domain>({
|
const form = useForm<Domain>({
|
||||||
resolver: zodResolver(domainCompose),
|
resolver: zodResolver(domainCompose),
|
||||||
|
defaultValues: {
|
||||||
|
host: "",
|
||||||
|
path: undefined,
|
||||||
|
port: undefined,
|
||||||
|
https: false,
|
||||||
|
certificateType: undefined,
|
||||||
|
customCertResolver: undefined,
|
||||||
|
serviceName: "",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const https = form.watch("https");
|
const https = form.watch("https");
|
||||||
@@ -116,11 +125,21 @@ export const AddDomainCompose = ({
|
|||||||
path: data?.path || undefined,
|
path: data?.path || undefined,
|
||||||
port: data?.port || undefined,
|
port: data?.port || undefined,
|
||||||
serviceName: data?.serviceName || undefined,
|
serviceName: data?.serviceName || undefined,
|
||||||
|
certificateType: data?.certificateType || undefined,
|
||||||
|
customCertResolver: data?.customCertResolver || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!domainId) {
|
if (!domainId) {
|
||||||
form.reset({});
|
form.reset({
|
||||||
|
host: "",
|
||||||
|
path: undefined,
|
||||||
|
port: undefined,
|
||||||
|
https: false,
|
||||||
|
certificateType: undefined,
|
||||||
|
customCertResolver: undefined,
|
||||||
|
serviceName: "",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data, isLoading]);
|
}, [form, form.reset, data, isLoading]);
|
||||||
|
|
||||||
@@ -393,33 +412,55 @@ export const AddDomainCompose = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{https && (
|
{https && (
|
||||||
<FormField
|
<>
|
||||||
control={form.control}
|
<FormField
|
||||||
name="certificateType"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="certificateType"
|
||||||
<FormItem className="col-span-2">
|
render={({ field }) => (
|
||||||
<FormLabel>Certificate Provider</FormLabel>
|
<FormItem className="col-span-2">
|
||||||
<Select
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
onValueChange={field.onChange}
|
<Select
|
||||||
defaultValue={field.value || ""}
|
onValueChange={field.onChange}
|
||||||
>
|
defaultValue={field.value || ""}
|
||||||
<FormControl>
|
>
|
||||||
<SelectTrigger>
|
<FormControl>
|
||||||
<SelectValue placeholder="Select a certificate provider" />
|
<SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Select a certificate provider" />
|
||||||
</FormControl>
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">None</SelectItem>
|
<SelectItem value="none">None</SelectItem>
|
||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Let's Encrypt
|
Let's Encrypt
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
<SelectItem value={"custom"}>Custom</SelectItem>
|
||||||
</Select>
|
</SelectContent>
|
||||||
<FormMessage />
|
</Select>
|
||||||
</FormItem>
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.getValues().certificateType === "custom" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="customCertResolver"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="col-span-2">
|
||||||
|
<FormLabel>Custom Certificate Resolver</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter your custom certificate resolver"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, Hammer, Terminal } from "lucide-react";
|
import { Ban, CheckCircle2, Hammer, HelpCircle, Terminal } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
@@ -27,103 +34,159 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
api.compose.stop.useMutation();
|
api.compose.stop.useMutation();
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
||||||
<DialogAction
|
<TooltipProvider delayDuration={0}>
|
||||||
title="Deploy Compose"
|
|
||||||
description="Are you sure you want to deploy this compose?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await deploy({
|
|
||||||
composeId: composeId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Compose deployed successfully");
|
|
||||||
refetch();
|
|
||||||
router.push(
|
|
||||||
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deploying compose");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="default" isLoading={data?.composeStatus === "running"}>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Rebuild Compose"
|
|
||||||
description="Are you sure you want to rebuild this compose?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await redeploy({
|
|
||||||
composeId: composeId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Compose rebuilt successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error rebuilding compose");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={data?.composeStatus === "running"}
|
|
||||||
>
|
|
||||||
Rebuild
|
|
||||||
<Hammer className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
{data?.composeType === "docker-compose" &&
|
|
||||||
data?.composeStatus === "idle" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Compose"
|
title="Deploy Compose"
|
||||||
description="Are you sure you want to start this compose?"
|
description="Are you sure you want to deploy this compose?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
await deploy({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose started successfully");
|
toast.success("Compose deployed successfully");
|
||||||
refetch();
|
refetch();
|
||||||
|
router.push(
|
||||||
|
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error starting compose");
|
toast.error("Error deploying compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isStarting}>
|
<Button
|
||||||
Start
|
variant="default"
|
||||||
<CheckCircle2 className="size-4" />
|
isLoading={data?.composeStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Downloads the source code and performs a complete build</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Compose"
|
title="Rebuild Compose"
|
||||||
description="Are you sure you want to stop this compose?"
|
description="Are you sure you want to rebuild this compose?"
|
||||||
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await redeploy({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose stopped successfully");
|
toast.success("Compose rebuilt successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error stopping compose");
|
toast.error("Error rebuilding compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="destructive" isLoading={isStopping}>
|
<Button
|
||||||
Stop
|
variant="secondary"
|
||||||
<Ban className="size-4" />
|
isLoading={data?.composeStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Rebuild
|
||||||
|
<Hammer className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Only rebuilds the compose without downloading new code</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
{data?.composeType === "docker-compose" &&
|
||||||
|
data?.composeStatus === "idle" ? (
|
||||||
|
<DialogAction
|
||||||
|
title="Start Compose"
|
||||||
|
description="Are you sure you want to start this compose?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
composeId: composeId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Compose started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting compose");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the compose (requires a previous successful build)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Compose"
|
||||||
|
description="Are you sure you want to stop this compose?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
composeId: composeId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Compose stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping compose");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running compose</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
|
|||||||
@@ -29,14 +29,23 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
const BitbucketProviderSchema = z.object({
|
const BitbucketProviderSchema = z.object({
|
||||||
composePath: z.string().min(1),
|
composePath: z.string().min(1),
|
||||||
@@ -48,6 +57,7 @@ const BitbucketProviderSchema = z.object({
|
|||||||
.required(),
|
.required(),
|
||||||
branch: z.string().min(1, "Branch is required"),
|
branch: z.string().min(1, "Branch is required"),
|
||||||
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
||||||
|
watchPaths: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
|
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
|
||||||
@@ -73,6 +83,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
},
|
},
|
||||||
bitbucketId: "",
|
bitbucketId: "",
|
||||||
branch: "",
|
branch: "",
|
||||||
|
watchPaths: [],
|
||||||
},
|
},
|
||||||
resolver: zodResolver(BitbucketProviderSchema),
|
resolver: zodResolver(BitbucketProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -118,6 +129,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
},
|
},
|
||||||
composePath: data.composePath,
|
composePath: data.composePath,
|
||||||
bitbucketId: data.bitbucketId || "",
|
bitbucketId: data.bitbucketId || "",
|
||||||
|
watchPaths: data.watchPaths || [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -132,6 +144,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
composeId,
|
composeId,
|
||||||
sourceType: "bitbucket",
|
sourceType: "bitbucket",
|
||||||
composeStatus: "idle",
|
composeStatus: "idle",
|
||||||
|
watchPaths: data.watchPaths,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provided Saved");
|
||||||
@@ -197,7 +210,20 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
name="repository"
|
name="repository"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="md:col-span-2 flex flex-col">
|
<FormItem className="md:col-span-2 flex flex-col">
|
||||||
<FormLabel>Repository</FormLabel>
|
<div className="flex items-center justify-between">
|
||||||
|
<FormLabel>Repository</FormLabel>
|
||||||
|
{field.value.owner && field.value.repo && (
|
||||||
|
<Link
|
||||||
|
href={`https://bitbucket.org/${field.value.owner}/${field.value.repo}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
|
>
|
||||||
|
<BitbucketIcon className="h-4 w-4" />
|
||||||
|
<span>View Repository</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -365,6 +391,84 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="watchPaths"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||||
|
?
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Add paths to watch for changes. When files in these
|
||||||
|
paths change, a new deployment will be triggered.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{field.value?.map((path, index) => (
|
||||||
|
<Badge key={index} variant="secondary">
|
||||||
|
{path}
|
||||||
|
<X
|
||||||
|
className="ml-1 size-3 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
const newPaths = [...(field.value || [])];
|
||||||
|
newPaths.splice(index, 1);
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.currentTarget;
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
const newPaths = [...(field.value || []), value];
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.querySelector(
|
||||||
|
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
const newPaths = [...(field.value || []), value];
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -17,14 +18,22 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { KeyRoundIcon, LockIcon } from "lucide-react";
|
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
const GitProviderSchema = z.object({
|
const GitProviderSchema = z.object({
|
||||||
composePath: z.string().min(1),
|
composePath: z.string().min(1),
|
||||||
@@ -33,6 +42,7 @@ const GitProviderSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
branch: z.string().min(1, "Branch required"),
|
branch: z.string().min(1, "Branch required"),
|
||||||
sshKey: z.string().optional(),
|
sshKey: z.string().optional(),
|
||||||
|
watchPaths: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GitProvider = z.infer<typeof GitProviderSchema>;
|
type GitProvider = z.infer<typeof GitProviderSchema>;
|
||||||
@@ -54,6 +64,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
repositoryURL: "",
|
repositoryURL: "",
|
||||||
composePath: "./docker-compose.yml",
|
composePath: "./docker-compose.yml",
|
||||||
sshKey: undefined,
|
sshKey: undefined,
|
||||||
|
watchPaths: [],
|
||||||
},
|
},
|
||||||
resolver: zodResolver(GitProviderSchema),
|
resolver: zodResolver(GitProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -65,6 +76,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
branch: data.customGitBranch || "",
|
branch: data.customGitBranch || "",
|
||||||
repositoryURL: data.customGitUrl || "",
|
repositoryURL: data.customGitUrl || "",
|
||||||
composePath: data.composePath,
|
composePath: data.composePath,
|
||||||
|
watchPaths: data.watchPaths || [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -77,6 +89,8 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
composeId,
|
composeId,
|
||||||
sourceType: "git",
|
sourceType: "git",
|
||||||
composePath: values.composePath,
|
composePath: values.composePath,
|
||||||
|
composeStatus: "idle",
|
||||||
|
watchPaths: values.watchPaths || [],
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Git Provider Saved");
|
toast.success("Git Provider Saved");
|
||||||
@@ -101,11 +115,22 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
name="repositoryURL"
|
name="repositoryURL"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="flex flex-row justify-between">
|
<div className="flex items-center justify-between">
|
||||||
Repository URL
|
<FormLabel>Repository URL</FormLabel>
|
||||||
</FormLabel>
|
{field.value?.startsWith("https://") && (
|
||||||
|
<Link
|
||||||
|
href={field.value}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
|
>
|
||||||
|
<GitIcon className="h-4 w-4" />
|
||||||
|
<span>View Repository</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="git@bitbucket.org" {...field} />
|
<Input placeholder="Repository URL" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -191,6 +216,85 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="watchPaths"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||||
|
?
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-[300px]">
|
||||||
|
<p>
|
||||||
|
Add paths to watch for changes. When files in these
|
||||||
|
paths change, a new deployment will be triggered. This
|
||||||
|
will work only when manual webhook is setup.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{field.value?.map((path, index) => (
|
||||||
|
<Badge key={index} variant="secondary">
|
||||||
|
{path}
|
||||||
|
<X
|
||||||
|
className="ml-1 size-3 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
const newPaths = [...(field.value || [])];
|
||||||
|
newPaths.splice(index, 1);
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.currentTarget;
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
const newPaths = [...(field.value || []), value];
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.querySelector(
|
||||||
|
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
const newPaths = [...(field.value || []), value];
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end">
|
<div className="flex flex-row justify-end">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -28,14 +29,22 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
const GithubProviderSchema = z.object({
|
const GithubProviderSchema = z.object({
|
||||||
composePath: z.string().min(1),
|
composePath: z.string().min(1),
|
||||||
@@ -47,6 +56,7 @@ const GithubProviderSchema = z.object({
|
|||||||
.required(),
|
.required(),
|
||||||
branch: z.string().min(1, "Branch is required"),
|
branch: z.string().min(1, "Branch is required"),
|
||||||
githubId: z.string().min(1, "Github Provider is required"),
|
githubId: z.string().min(1, "Github Provider is required"),
|
||||||
|
watchPaths: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GithubProvider = z.infer<typeof GithubProviderSchema>;
|
type GithubProvider = z.infer<typeof GithubProviderSchema>;
|
||||||
@@ -71,6 +81,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
},
|
},
|
||||||
githubId: "",
|
githubId: "",
|
||||||
branch: "",
|
branch: "",
|
||||||
|
watchPaths: [],
|
||||||
},
|
},
|
||||||
resolver: zodResolver(GithubProviderSchema),
|
resolver: zodResolver(GithubProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -113,6 +124,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
},
|
},
|
||||||
composePath: data.composePath,
|
composePath: data.composePath,
|
||||||
githubId: data.githubId || "",
|
githubId: data.githubId || "",
|
||||||
|
watchPaths: data.watchPaths || [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -127,6 +139,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
githubId: data.githubId,
|
githubId: data.githubId,
|
||||||
sourceType: "github",
|
sourceType: "github",
|
||||||
composeStatus: "idle",
|
composeStatus: "idle",
|
||||||
|
watchPaths: data.watchPaths,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provided Saved");
|
||||||
@@ -183,13 +196,25 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="repository"
|
name="repository"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="md:col-span-2 flex flex-col">
|
<FormItem className="md:col-span-2 flex flex-col">
|
||||||
<FormLabel>Repository</FormLabel>
|
<div className="flex items-center justify-between">
|
||||||
|
<FormLabel>Repository</FormLabel>
|
||||||
|
{field.value.owner && field.value.repo && (
|
||||||
|
<Link
|
||||||
|
href={`https://github.com/${field.value.owner}/${field.value.repo}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
|
>
|
||||||
|
<GithubIcon className="h-4 w-4" />
|
||||||
|
<span>View Repository</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -357,6 +382,84 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="watchPaths"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||||
|
?
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Add paths to watch for changes. When files in these
|
||||||
|
paths change, a new deployment will be triggered.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{field.value?.map((path, index) => (
|
||||||
|
<Badge key={index} variant="secondary">
|
||||||
|
{path}
|
||||||
|
<X
|
||||||
|
className="ml-1 size-3 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
const newPaths = [...(field.value || [])];
|
||||||
|
newPaths.splice(index, 1);
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.currentTarget;
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
const newPaths = [...(field.value || []), value];
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.querySelector(
|
||||||
|
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
const newPaths = [...(field.value || []), value];
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -29,14 +29,23 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
const GitlabProviderSchema = z.object({
|
const GitlabProviderSchema = z.object({
|
||||||
composePath: z.string().min(1),
|
composePath: z.string().min(1),
|
||||||
@@ -50,6 +59,7 @@ const GitlabProviderSchema = z.object({
|
|||||||
.required(),
|
.required(),
|
||||||
branch: z.string().min(1, "Branch is required"),
|
branch: z.string().min(1, "Branch is required"),
|
||||||
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
||||||
|
watchPaths: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
|
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
|
||||||
@@ -76,6 +86,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
},
|
},
|
||||||
gitlabId: "",
|
gitlabId: "",
|
||||||
branch: "",
|
branch: "",
|
||||||
|
watchPaths: [],
|
||||||
},
|
},
|
||||||
resolver: zodResolver(GitlabProviderSchema),
|
resolver: zodResolver(GitlabProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -124,6 +135,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
},
|
},
|
||||||
composePath: data.composePath,
|
composePath: data.composePath,
|
||||||
gitlabId: data.gitlabId || "",
|
gitlabId: data.gitlabId || "",
|
||||||
|
watchPaths: data.watchPaths || [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -140,6 +152,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
gitlabPathNamespace: data.repository.gitlabPathNamespace,
|
gitlabPathNamespace: data.repository.gitlabPathNamespace,
|
||||||
sourceType: "gitlab",
|
sourceType: "gitlab",
|
||||||
composeStatus: "idle",
|
composeStatus: "idle",
|
||||||
|
watchPaths: data.watchPaths,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provided Saved");
|
||||||
@@ -199,13 +212,25 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="repository"
|
name="repository"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="md:col-span-2 flex flex-col">
|
<FormItem className="md:col-span-2 flex flex-col">
|
||||||
<FormLabel>Repository</FormLabel>
|
<div className="flex items-center justify-between">
|
||||||
|
<FormLabel>Repository</FormLabel>
|
||||||
|
{field.value.owner && field.value.repo && (
|
||||||
|
<Link
|
||||||
|
href={`https://gitlab.com/${field.value.owner}/${field.value.repo}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
|
>
|
||||||
|
<GitlabIcon className="h-4 w-4" />
|
||||||
|
<span>View Repository</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -382,6 +407,84 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="watchPaths"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||||
|
?
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Add paths to watch for changes. When files in these
|
||||||
|
paths change, a new deployment will be triggered.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{field.value?.map((path, index) => (
|
||||||
|
<Badge key={index} variant="secondary">
|
||||||
|
{path}
|
||||||
|
<X
|
||||||
|
className="ml-1 size-3 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
const newPaths = [...(field.value || [])];
|
||||||
|
newPaths.splice(index, 1);
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.currentTarget;
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
const newPaths = [...(field.value || []), value];
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.querySelector(
|
||||||
|
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
const newPaths = [...(field.value || []), value];
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -147,9 +147,9 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Enable Randomize ({data?.appName})</FormLabel>
|
<FormLabel>Enable Isolated Deployment ({data?.appName})</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Enable randomize to the compose file.
|
Enable isolated deployment to the compose file.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
|
|||||||
@@ -2,8 +2,21 @@ import { DialogAction } from "@/components/shared/dialog-action";
|
|||||||
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import {
|
||||||
|
Ban,
|
||||||
|
CheckCircle2,
|
||||||
|
HelpCircle,
|
||||||
|
RefreshCcw,
|
||||||
|
Terminal,
|
||||||
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
@@ -65,92 +78,150 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<DialogAction
|
<TooltipProvider delayDuration={0}>
|
||||||
title="Deploy Mariadb"
|
|
||||||
description="Are you sure you want to deploy this mariadb?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsDeploying(true);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Mariadb"
|
|
||||||
description="Are you sure you want to reload this mariadb?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
mariadbId: mariadbId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Mariadb reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading Mariadb");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
|
||||||
Reload
|
|
||||||
<RefreshCcw className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Mariadb"
|
title="Deploy Mariadb"
|
||||||
description="Are you sure you want to start this mariadb?"
|
description="Are you sure you want to deploy this mariadb?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
mariadbId: mariadbId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("Mariadb started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Mariadb");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isStarting}>
|
<Button
|
||||||
Start
|
variant="default"
|
||||||
<CheckCircle2 className="size-4" />
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Downloads and sets up the MariaDB database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Mariadb"
|
title="Reload Mariadb"
|
||||||
description="Are you sure you want to stop this mariadb?"
|
description="Are you sure you want to reload this mariadb?"
|
||||||
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await reload({
|
||||||
mariadbId: mariadbId,
|
mariadbId: mariadbId,
|
||||||
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mariadb stopped successfully");
|
toast.success("Mariadb reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error stopping Mariadb");
|
toast.error("Error reloading Mariadb");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="destructive" isLoading={isStopping}>
|
<Button
|
||||||
Stop
|
variant="secondary"
|
||||||
<Ban className="size-4" />
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
<RefreshCcw className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the MariaDB service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
{data?.applicationStatus === "idle" ? (
|
||||||
|
<DialogAction
|
||||||
|
title="Start Mariadb"
|
||||||
|
description="Are you sure you want to start this mariadb?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
mariadbId: mariadbId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mariadb started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting Mariadb");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the MariaDB database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Mariadb"
|
||||||
|
description="Are you sure you want to stop this mariadb?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
mariadbId: mariadbId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mariadb stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Mariadb");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running MariaDB database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
|
|||||||
@@ -2,8 +2,21 @@ import { DialogAction } from "@/components/shared/dialog-action";
|
|||||||
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import {
|
||||||
|
Ban,
|
||||||
|
CheckCircle2,
|
||||||
|
HelpCircle,
|
||||||
|
RefreshCcw,
|
||||||
|
Terminal,
|
||||||
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
@@ -64,93 +77,150 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<DialogAction
|
<TooltipProvider delayDuration={0}>
|
||||||
title="Deploy Mongo"
|
|
||||||
description="Are you sure you want to deploy this mongo?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsDeploying(true);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Mongo"
|
|
||||||
description="Are you sure you want to reload this mongo?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
mongoId: mongoId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Mongo reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading Mongo");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
|
||||||
Reload
|
|
||||||
<RefreshCcw className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Mongo"
|
title="Deploy Mongo"
|
||||||
description="Are you sure you want to start this mongo?"
|
description="Are you sure you want to deploy this mongo?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Downloads and sets up the MongoDB database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
<DialogAction
|
||||||
|
title="Reload Mongo"
|
||||||
|
description="Are you sure you want to reload this mongo?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
mongoId: mongoId,
|
mongoId: mongoId,
|
||||||
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mongo started successfully");
|
toast.success("Mongo reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error starting Mongo");
|
toast.error("Error reloading Mongo");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isStarting}>
|
<Button
|
||||||
Start
|
variant="secondary"
|
||||||
<CheckCircle2 className="size-4" />
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
<RefreshCcw className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the MongoDB service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
{data?.applicationStatus === "idle" ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Mongo"
|
title="Start Mongo"
|
||||||
description="Are you sure you want to stop this mongo?"
|
description="Are you sure you want to start this mongo?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await start({
|
||||||
mongoId: mongoId,
|
mongoId: mongoId,
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Mongo stopped successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error stopping Mongo");
|
toast.success("Mongo started successfully");
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button variant="destructive" isLoading={isStopping}>
|
toast.error("Error starting Mongo");
|
||||||
Stop
|
});
|
||||||
<Ban className="size-4" />
|
}}
|
||||||
</Button>
|
>
|
||||||
</DialogAction>
|
<Button
|
||||||
)}
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the MongoDB database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Mongo"
|
||||||
|
description="Are you sure you want to stop this mongo?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
mongoId: mongoId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mongo stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Mongo");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running MongoDB database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ export const ContainerFreeMonitoring = ({
|
|||||||
}, [appName]);
|
}, [appName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl bg-background shadow-md flex flex-col gap-4">
|
<div className="rounded-xl bg-background flex flex-col gap-4">
|
||||||
<header className="flex items-center justify-between">
|
<header className="flex items-center justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">Monitoring</h1>
|
<h1 className="text-2xl font-semibold tracking-tight">Monitoring</h1>
|
||||||
|
|||||||
@@ -2,8 +2,21 @@ import { DialogAction } from "@/components/shared/dialog-action";
|
|||||||
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import {
|
||||||
|
Ban,
|
||||||
|
CheckCircle2,
|
||||||
|
HelpCircle,
|
||||||
|
RefreshCcw,
|
||||||
|
Terminal,
|
||||||
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
@@ -62,93 +75,150 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<DialogAction
|
<TooltipProvider delayDuration={0}>
|
||||||
title="Deploy Mysql"
|
|
||||||
description="Are you sure you want to deploy this mysql?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsDeploying(true);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Mysql"
|
|
||||||
description="Are you sure you want to reload this mysql?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
mysqlId: mysqlId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Mysql reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading Mysql");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
|
||||||
Reload
|
|
||||||
<RefreshCcw className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Mysql"
|
title="Deploy Mysql"
|
||||||
description="Are you sure you want to start this mysql?"
|
description="Are you sure you want to deploy this mysql?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
mysqlId: mysqlId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("Mysql started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Mysql");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isStarting}>
|
<Button
|
||||||
Start
|
variant="default"
|
||||||
<CheckCircle2 className="size-4" />
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Downloads and sets up the MySQL database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Mysql"
|
title="Reload Mysql"
|
||||||
description="Are you sure you want to stop this mysql?"
|
description="Are you sure you want to reload this mysql?"
|
||||||
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await reload({
|
||||||
mysqlId: mysqlId,
|
mysqlId: mysqlId,
|
||||||
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mysql stopped successfully");
|
toast.success("Mysql reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error stopping Mysql");
|
toast.error("Error reloading Mysql");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="destructive" isLoading={isStopping}>
|
<Button
|
||||||
Stop
|
variant="secondary"
|
||||||
<Ban className="size-4" />
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
<RefreshCcw className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the MySQL service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
{data?.applicationStatus === "idle" ? (
|
||||||
|
<DialogAction
|
||||||
|
title="Start Mysql"
|
||||||
|
description="Are you sure you want to start this mysql?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
mysqlId: mysqlId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mysql started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting Mysql");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the MySQL database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Mysql"
|
||||||
|
description="Are you sure you want to stop this mysql?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
mysqlId: mysqlId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mysql stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Mysql");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running MySQL database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
|
|||||||
@@ -2,12 +2,26 @@ import { DialogAction } from "@/components/shared/dialog-action";
|
|||||||
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import {
|
||||||
|
Ban,
|
||||||
|
CheckCircle2,
|
||||||
|
HelpCircle,
|
||||||
|
RefreshCcw,
|
||||||
|
Terminal,
|
||||||
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
}
|
}
|
||||||
@@ -57,122 +71,179 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<>
|
||||||
<Card className="bg-background">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<CardHeader className="pb-4">
|
<Card className="bg-background">
|
||||||
<CardTitle className="text-xl">General</CardTitle>
|
<CardHeader>
|
||||||
</CardHeader>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
<CardContent className="flex gap-4">
|
</CardHeader>
|
||||||
<DialogAction
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
title="Deploy Postgres"
|
<TooltipProvider delayDuration={0}>
|
||||||
description="Are you sure you want to deploy this postgres?"
|
<DialogAction
|
||||||
type="default"
|
title="Deploy Postgres"
|
||||||
onClick={async () => {
|
description="Are you sure you want to deploy this postgres?"
|
||||||
setIsDeploying(true);
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
setIsDeploying(true);
|
||||||
refetch();
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Postgres"
|
|
||||||
description="Are you sure you want to reload this postgres?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
postgresId: postgresId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Postgres reloaded successfully");
|
|
||||||
refetch();
|
refetch();
|
||||||
})
|
}}
|
||||||
.catch(() => {
|
>
|
||||||
toast.error("Error reloading Postgres");
|
<Button
|
||||||
});
|
variant="default"
|
||||||
}}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
>
|
className="flex items-center gap-1.5"
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
>
|
||||||
Reload
|
Deploy
|
||||||
<RefreshCcw className="size-4" />
|
<Tooltip>
|
||||||
</Button>
|
<TooltipTrigger asChild>
|
||||||
</DialogAction>
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
{data?.applicationStatus === "idle" ? (
|
</TooltipTrigger>
|
||||||
<DialogAction
|
<TooltipPrimitive.Portal>
|
||||||
title="Start Postgres"
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
description="Are you sure you want to start this postgres?"
|
<p>Downloads and sets up the PostgreSQL database</p>
|
||||||
type="default"
|
</TooltipContent>
|
||||||
onClick={async () => {
|
</TooltipPrimitive.Portal>
|
||||||
await start({
|
</Tooltip>
|
||||||
postgresId: postgresId,
|
</Button>
|
||||||
})
|
</DialogAction>
|
||||||
.then(() => {
|
<DialogAction
|
||||||
toast.success("Postgres started successfully");
|
title="Reload Postgres"
|
||||||
refetch();
|
description="Are you sure you want to reload this postgres?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
postgresId: postgresId,
|
||||||
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error starting Postgres");
|
toast.success("Postgres reloaded successfully");
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error reloading Postgres");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
<RefreshCcw className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the PostgreSQL service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
{data?.applicationStatus === "idle" ? (
|
||||||
|
<DialogAction
|
||||||
|
title="Start Postgres"
|
||||||
|
description="Are you sure you want to start this postgres?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
postgresId: postgresId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Postgres started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting Postgres");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the PostgreSQL database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Postgres"
|
||||||
|
description="Are you sure you want to stop this postgres?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
postgresId: postgresId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Postgres stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Postgres");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running PostgreSQL database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
<DockerTerminalModal
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isStarting}>
|
<Button variant="outline">
|
||||||
Start
|
<Terminal />
|
||||||
<CheckCircle2 className="size-4" />
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DockerTerminalModal>
|
||||||
) : (
|
</CardContent>
|
||||||
<DialogAction
|
</Card>
|
||||||
title="Stop Postgres"
|
<DrawerLogs
|
||||||
description="Are you sure you want to stop this postgres?"
|
isOpen={isDrawerOpen}
|
||||||
onClick={async () => {
|
onClose={() => {
|
||||||
await stop({
|
setIsDrawerOpen(false);
|
||||||
postgresId: postgresId,
|
setFilteredLogs([]);
|
||||||
})
|
setIsDeploying(false);
|
||||||
.then(() => {
|
refetch();
|
||||||
toast.success("Postgres stopped successfully");
|
}}
|
||||||
refetch();
|
filteredLogs={filteredLogs}
|
||||||
})
|
/>
|
||||||
.catch(() => {
|
</div>
|
||||||
toast.error("Error stopping Postgres");
|
</>
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="destructive" isLoading={isStopping}>
|
|
||||||
Stop
|
|
||||||
<Ban className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DockerTerminalModal
|
|
||||||
appName={data?.appName || ""}
|
|
||||||
serverId={data?.serverId || ""}
|
|
||||||
>
|
|
||||||
<Button variant="outline">
|
|
||||||
<Terminal />
|
|
||||||
Open Terminal
|
|
||||||
</Button>
|
|
||||||
</DockerTerminalModal>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<DrawerLogs
|
|
||||||
isOpen={isDrawerOpen}
|
|
||||||
onClose={() => {
|
|
||||||
setIsDrawerOpen(false);
|
|
||||||
setFilteredLogs([]);
|
|
||||||
setIsDeploying(false);
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
filteredLogs={filteredLogs}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -383,9 +383,8 @@ export const AddTemplate = ({ projectId }: Props) => {
|
|||||||
side="top"
|
side="top"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
If ot server is selected, the application
|
If no server is selected, the application will be
|
||||||
will be deployed on the server where the
|
deployed on the server where the user is logged in.
|
||||||
user is logged in.
|
|
||||||
</span>
|
</span>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -2,12 +2,26 @@ import { DialogAction } from "@/components/shared/dialog-action";
|
|||||||
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import {
|
||||||
|
Ban,
|
||||||
|
CheckCircle2,
|
||||||
|
HelpCircle,
|
||||||
|
RefreshCcw,
|
||||||
|
Terminal,
|
||||||
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
redisId: string;
|
redisId: string;
|
||||||
}
|
}
|
||||||
@@ -63,94 +77,150 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<DialogAction
|
<TooltipProvider delayDuration={0}>
|
||||||
title="Deploy Redis"
|
|
||||||
description="Are you sure you want to deploy this redis?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsDeploying(true);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Redis"
|
|
||||||
description="Are you sure you want to reload this redis?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
redisId: redisId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Redis reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading Redis");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
|
||||||
Reload
|
|
||||||
<RefreshCcw className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
{/* <ResetRedis redisId={redisId} appName={data?.appName || ""} /> */}
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Redis"
|
title="Deploy Redis"
|
||||||
description="Are you sure you want to start this redis?"
|
description="Are you sure you want to deploy this redis?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
redisId: redisId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("Redis started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Redis");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isStarting}>
|
<Button
|
||||||
Start
|
variant="default"
|
||||||
<CheckCircle2 className="size-4" />
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Downloads and sets up the Redis database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Redis"
|
title="Reload Redis"
|
||||||
description="Are you sure you want to stop this redis?"
|
description="Are you sure you want to reload this redis?"
|
||||||
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await reload({
|
||||||
redisId: redisId,
|
redisId: redisId,
|
||||||
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Redis stopped successfully");
|
toast.success("Redis reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error stopping Redis");
|
toast.error("Error reloading Redis");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="destructive" isLoading={isStopping}>
|
<Button
|
||||||
Stop
|
variant="secondary"
|
||||||
<Ban className="size-4" />
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
<RefreshCcw className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the Redis service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
{data?.applicationStatus === "idle" ? (
|
||||||
|
<DialogAction
|
||||||
|
title="Start Redis"
|
||||||
|
description="Are you sure you want to start this redis?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
redisId: redisId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Redis started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting Redis");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the Redis database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Redis"
|
||||||
|
description="Are you sure you want to stop this redis?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
redisId: redisId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Redis stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Redis");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running Redis database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import { api } from "@/utils/api";
|
||||||
import {
|
import {
|
||||||
type ChartConfig,
|
type ChartConfig,
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from "@/components/ui/chart";
|
} from "@/components/ui/chart";
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import {
|
import {
|
||||||
Area,
|
Area,
|
||||||
AreaChart,
|
AreaChart,
|
||||||
@@ -14,6 +14,13 @@ import {
|
|||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
|
||||||
|
export interface RequestDistributionChartProps {
|
||||||
|
dateRange?: {
|
||||||
|
from: Date | undefined;
|
||||||
|
to: Date | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
views: {
|
views: {
|
||||||
label: "Page Views",
|
label: "Page Views",
|
||||||
@@ -24,10 +31,22 @@ const chartConfig = {
|
|||||||
},
|
},
|
||||||
} satisfies ChartConfig;
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
export const RequestDistributionChart = () => {
|
export const RequestDistributionChart = ({
|
||||||
const { data: stats } = api.settings.readStats.useQuery(undefined, {
|
dateRange,
|
||||||
refetchInterval: 1333,
|
}: RequestDistributionChartProps) => {
|
||||||
});
|
const { data: stats } = api.settings.readStats.useQuery(
|
||||||
|
{
|
||||||
|
dateRange: dateRange
|
||||||
|
? {
|
||||||
|
start: dateRange.from?.toISOString(),
|
||||||
|
end: dateRange.to?.toISOString(),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refetchInterval: 1333,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponsiveContainer width="100%" height={200}>
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
|
|||||||
@@ -79,7 +79,15 @@ export const priorities = [
|
|||||||
icon: Server,
|
icon: Server,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
export const RequestsTable = () => {
|
|
||||||
|
export interface RequestsTableProps {
|
||||||
|
dateRange?: {
|
||||||
|
from: Date | undefined;
|
||||||
|
to: Date | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
|
||||||
const [statusFilter, setStatusFilter] = useState<string[]>([]);
|
const [statusFilter, setStatusFilter] = useState<string[]>([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [selectedRow, setSelectedRow] = useState<LogEntry>();
|
const [selectedRow, setSelectedRow] = useState<LogEntry>();
|
||||||
@@ -98,6 +106,12 @@ export const RequestsTable = () => {
|
|||||||
page: pagination,
|
page: pagination,
|
||||||
search,
|
search,
|
||||||
status: statusFilter,
|
status: statusFilter,
|
||||||
|
dateRange: dateRange
|
||||||
|
? {
|
||||||
|
start: dateRange.from?.toISOString(),
|
||||||
|
end: dateRange.to?.toISOString(),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
refetchInterval: 1333,
|
refetchInterval: 1333,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -8,9 +9,29 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { type RouterOutputs, api } from "@/utils/api";
|
import { type RouterOutputs, api } from "@/utils/api";
|
||||||
import { ArrowDownUp } from "lucide-react";
|
import { format } from "date-fns";
|
||||||
|
import {
|
||||||
|
ArrowDownUp,
|
||||||
|
AlertCircle,
|
||||||
|
InfoIcon,
|
||||||
|
Calendar as CalendarIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { RequestDistributionChart } from "./request-distribution-chart";
|
import { RequestDistributionChart } from "./request-distribution-chart";
|
||||||
import { RequestsTable } from "./requests-table";
|
import { RequestsTable } from "./requests-table";
|
||||||
@@ -20,17 +41,30 @@ export type LogEntry = NonNullable<
|
|||||||
>[0];
|
>[0];
|
||||||
|
|
||||||
export const ShowRequests = () => {
|
export const ShowRequests = () => {
|
||||||
const { data: isLogRotateActive, refetch: refetchLogRotate } =
|
|
||||||
api.settings.getLogRotateStatus.useQuery();
|
|
||||||
|
|
||||||
const { mutateAsync: toggleLogRotate } =
|
|
||||||
api.settings.toggleLogRotate.useMutation();
|
|
||||||
|
|
||||||
const { data: isActive, refetch } =
|
const { data: isActive, refetch } =
|
||||||
api.settings.haveActivateRequests.useQuery();
|
api.settings.haveActivateRequests.useQuery();
|
||||||
const { mutateAsync: toggleRequests } =
|
const { mutateAsync: toggleRequests } =
|
||||||
api.settings.toggleRequests.useMutation();
|
api.settings.toggleRequests.useMutation();
|
||||||
|
|
||||||
|
const { data: logCleanupStatus } =
|
||||||
|
api.settings.getLogCleanupStatus.useQuery();
|
||||||
|
const { mutateAsync: updateLogCleanup } =
|
||||||
|
api.settings.updateLogCleanup.useMutation();
|
||||||
|
const [cronExpression, setCronExpression] = useState<string | null>(null);
|
||||||
|
const [dateRange, setDateRange] = useState<{
|
||||||
|
from: Date | undefined;
|
||||||
|
to: Date | undefined;
|
||||||
|
}>({
|
||||||
|
from: undefined,
|
||||||
|
to: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (logCleanupStatus) {
|
||||||
|
setCronExpression(logCleanupStatus.cronExpression || "0 0 * * *");
|
||||||
|
}
|
||||||
|
}, [logCleanupStatus]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -57,7 +91,60 @@ export const ShowRequests = () => {
|
|||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 py-8 border-t">
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
<div className="flex w-full gap-4 justify-end">
|
<div className="flex w-full gap-4 justify-end items-center">
|
||||||
|
<div className="flex-1 flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label htmlFor="cron" className="min-w-32">
|
||||||
|
Log Cleanup Schedule
|
||||||
|
</Label>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="size-4 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="max-w-80">
|
||||||
|
At the scheduled time, the cleanup job will keep
|
||||||
|
only the last 1000 entries in the access log file
|
||||||
|
and signal Traefik to reopen its log files. The
|
||||||
|
default schedule is daily at midnight (0 0 * * *).
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex gap-4">
|
||||||
|
<Input
|
||||||
|
id="cron"
|
||||||
|
placeholder="0 0 * * *"
|
||||||
|
value={cronExpression || ""}
|
||||||
|
onChange={(e) => setCronExpression(e.target.value)}
|
||||||
|
className="max-w-60"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={async () => {
|
||||||
|
if (!cronExpression?.trim()) {
|
||||||
|
toast.error("Please enter a valid cron expression");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await updateLogCleanup({
|
||||||
|
cronExpression: cronExpression,
|
||||||
|
});
|
||||||
|
toast.success("Log cleanup schedule updated");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
`Failed to update log cleanup schedule: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update Schedule
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title={isActive ? "Deactivate Requests" : "Activate Requests"}
|
title={isActive ? "Deactivate Requests" : "Activate Requests"}
|
||||||
description="You will also need to restart Traefik to apply the changes"
|
description="You will also need to restart Traefik to apply the changes"
|
||||||
@@ -77,53 +164,81 @@ export const ShowRequests = () => {
|
|||||||
>
|
>
|
||||||
<Button>{isActive ? "Deactivate" : "Activate"}</Button>
|
<Button>{isActive ? "Deactivate" : "Activate"}</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
|
|
||||||
<DialogAction
|
|
||||||
title={
|
|
||||||
isLogRotateActive
|
|
||||||
? "Activate Log Rotate"
|
|
||||||
: "Deactivate Log Rotate"
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
isLogRotateActive
|
|
||||||
? "This will make the logs rotate on interval 1 day and maximum size of 100 MB and maximum 6 logs"
|
|
||||||
: "The log rotation will be disabled"
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
toggleLogRotate({
|
|
||||||
enable: !isLogRotateActive,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success(
|
|
||||||
`Log rotate ${isLogRotateActive ? "activated" : "deactivated"}`,
|
|
||||||
);
|
|
||||||
refetchLogRotate();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error(err.message);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="secondary">
|
|
||||||
{isLogRotateActive
|
|
||||||
? "Activate Log Rotate"
|
|
||||||
: "Deactivate Log Rotate"}
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{isActive ? (
|
||||||
{isActive ? (
|
<>
|
||||||
<RequestDistributionChart />
|
<div className="flex justify-end mb-4 gap-2">
|
||||||
) : (
|
{(dateRange.from || dateRange.to) && (
|
||||||
<div className="flex items-center justify-center min-h-[25vh]">
|
<Button
|
||||||
<span className="text-muted-foreground py-6">
|
variant="outline"
|
||||||
You need to activate requests
|
onClick={() =>
|
||||||
</span>
|
setDateRange({ from: undefined, to: undefined })
|
||||||
|
}
|
||||||
|
className="px-3"
|
||||||
|
>
|
||||||
|
Clear dates
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-[300px] justify-start text-left font-normal"
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{dateRange.from ? (
|
||||||
|
dateRange.to ? (
|
||||||
|
<>
|
||||||
|
{format(dateRange.from, "LLL dd, y")} -{" "}
|
||||||
|
{format(dateRange.to, "LLL dd, y")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
format(dateRange.from, "LLL dd, y")
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<span>Pick a date range</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="end">
|
||||||
|
<Calendar
|
||||||
|
initialFocus
|
||||||
|
mode="range"
|
||||||
|
defaultMonth={dateRange.from}
|
||||||
|
selected={{
|
||||||
|
from: dateRange.from,
|
||||||
|
to: dateRange.to,
|
||||||
|
}}
|
||||||
|
onSelect={(range) => {
|
||||||
|
setDateRange({
|
||||||
|
from: range?.from,
|
||||||
|
to: range?.to,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
numberOfMonths={2}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<RequestDistributionChart dateRange={dateRange} />
|
||||||
{isActive && <RequestsTable />}
|
<RequestsTable dateRange={dateRange} />
|
||||||
</div>
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 gap-4 text-muted-foreground">
|
||||||
|
<AlertCircle className="size-12 text-muted-foreground/50" />
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<h3 className="text-lg font-medium">
|
||||||
|
Requests are not activated
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm max-w-md">
|
||||||
|
Activate requests to see incoming traffic statistics and
|
||||||
|
monitor your application's usage. After activation, you'll
|
||||||
|
need to reload Traefik for the changes to take effect.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import {
|
|||||||
FormDescription,
|
FormDescription,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
@@ -441,13 +443,16 @@ export const AddApiKey = () => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-4">
|
||||||
<div className="rounded-md bg-muted p-4 font-mono text-sm break-all">
|
<CodeEditor
|
||||||
{newApiKey}
|
className="font-mono text-sm break-all"
|
||||||
</div>
|
language="properties"
|
||||||
|
value={newApiKey}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(newApiKey);
|
copy(newApiKey);
|
||||||
toast.success("API key copied to clipboard");
|
toast.success("API key copied to clipboard");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ import { S3_PROVIDERS } from "./constants";
|
|||||||
|
|
||||||
const addDestination = z.object({
|
const addDestination = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
provider: z.string().optional(),
|
provider: z.string().min(1, "Provider is required"),
|
||||||
accessKeyId: z.string(),
|
accessKeyId: z.string().min(1, "Access Key Id is required"),
|
||||||
secretAccessKey: z.string(),
|
secretAccessKey: z.string().min(1, "Secret Access Key is required"),
|
||||||
bucket: z.string(),
|
bucket: z.string().min(1, "Bucket is required"),
|
||||||
region: z.string(),
|
region: z.string(),
|
||||||
endpoint: z.string(),
|
endpoint: z.string().min(1, "Endpoint is required"),
|
||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,6 +129,63 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTestConnection = async (serverId?: string) => {
|
||||||
|
const result = await form.trigger([
|
||||||
|
"provider",
|
||||||
|
"accessKeyId",
|
||||||
|
"secretAccessKey",
|
||||||
|
"bucket",
|
||||||
|
"endpoint",
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
const errors = form.formState.errors;
|
||||||
|
const errorFields = Object.entries(errors)
|
||||||
|
.map(([field, error]) => `${field}: ${error?.message}`)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
toast.error("Please fill all required fields", {
|
||||||
|
description: errorFields,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCloud && !serverId) {
|
||||||
|
toast.error("Please select a server");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = form.getValues("provider");
|
||||||
|
const accessKey = form.getValues("accessKeyId");
|
||||||
|
const secretKey = form.getValues("secretAccessKey");
|
||||||
|
const bucket = form.getValues("bucket");
|
||||||
|
const endpoint = form.getValues("endpoint");
|
||||||
|
const region = form.getValues("region");
|
||||||
|
|
||||||
|
const connectionString = `:s3,provider=${provider},access_key_id=${accessKey},secret_access_key=${secretKey},endpoint=${endpoint}${region ? `,region=${region}` : ""}:${bucket}`;
|
||||||
|
|
||||||
|
await testConnection({
|
||||||
|
provider,
|
||||||
|
accessKey,
|
||||||
|
bucket,
|
||||||
|
endpoint,
|
||||||
|
name: "Test",
|
||||||
|
region,
|
||||||
|
secretAccessKey: secretKey,
|
||||||
|
serverId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Connection Success");
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast.error("Error connecting to provider", {
|
||||||
|
description: `${e.message}\n\nTry manually: rclone ls ${connectionString}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
@@ -349,26 +406,9 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
isLoading={isLoading}
|
isLoading={isLoadingConnection}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await testConnection({
|
await handleTestConnection(form.getValues("serverId"));
|
||||||
provider: form.getValues("provider") || "",
|
|
||||||
accessKey: form.getValues("accessKeyId"),
|
|
||||||
bucket: form.getValues("bucket"),
|
|
||||||
endpoint: form.getValues("endpoint"),
|
|
||||||
name: "Test",
|
|
||||||
region: form.getValues("region"),
|
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
|
||||||
serverId: form.getValues("serverId"),
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Connection Success");
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast.error("Error connecting the provider", {
|
|
||||||
description: e.message,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Test Connection
|
Test Connection
|
||||||
@@ -380,21 +420,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await testConnection({
|
await handleTestConnection();
|
||||||
provider: form.getValues("provider") || "",
|
|
||||||
accessKey: form.getValues("accessKeyId"),
|
|
||||||
bucket: form.getValues("bucket"),
|
|
||||||
endpoint: form.getValues("endpoint"),
|
|
||||||
name: "Test",
|
|
||||||
region: form.getValues("region"),
|
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Connection Success");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error connecting the provider");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Test connection
|
Test connection
|
||||||
|
|||||||
@@ -56,9 +56,17 @@ export const ShowDestinations = () => {
|
|||||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||||
<span className="text-sm">
|
<div className="flex flex-col gap-1">
|
||||||
{index + 1}. {destination.name}
|
<span className="text-sm">
|
||||||
</span>
|
{index + 1}. {destination.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Created at:{" "}
|
||||||
|
{new Date(
|
||||||
|
destination.createdAt,
|
||||||
|
).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
<HandleDestinations
|
<HandleDestinations
|
||||||
destinationId={destination.destinationId}
|
destinationId={destination.destinationId}
|
||||||
|
|||||||
@@ -45,21 +45,12 @@ const Schema = z.object({
|
|||||||
|
|
||||||
type Schema = z.infer<typeof Schema>;
|
type Schema = z.infer<typeof Schema>;
|
||||||
|
|
||||||
interface Model {
|
|
||||||
id: string;
|
|
||||||
object: string;
|
|
||||||
created: number;
|
|
||||||
owned_by: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
aiId?: string;
|
aiId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HandleAi = ({ aiId }: Props) => {
|
export const HandleAi = ({ aiId }: Props) => {
|
||||||
const [models, setModels] = useState<Model[]>([]);
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { data, refetch } = api.ai.one.useQuery(
|
const { data, refetch } = api.ai.one.useQuery(
|
||||||
@@ -73,6 +64,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
const { mutateAsync, isLoading } = aiId
|
const { mutateAsync, isLoading } = aiId
|
||||||
? api.ai.update.useMutation()
|
? api.ai.update.useMutation()
|
||||||
: api.ai.create.useMutation();
|
: api.ai.create.useMutation();
|
||||||
|
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
resolver: zodResolver(Schema),
|
resolver: zodResolver(Schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -94,50 +86,33 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
});
|
});
|
||||||
}, [aiId, form, data]);
|
}, [aiId, form, data]);
|
||||||
|
|
||||||
const fetchModels = async (apiUrl: string, apiKey: string) => {
|
const apiUrl = form.watch("apiUrl");
|
||||||
setIsLoadingModels(true);
|
const apiKey = form.watch("apiKey");
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${apiUrl}/models`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${apiKey}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to fetch models");
|
|
||||||
}
|
|
||||||
const res = await response.json();
|
|
||||||
setModels(res.data);
|
|
||||||
|
|
||||||
// Set default model to gpt-4 if present
|
const { data: models, isLoading: isLoadingServerModels } =
|
||||||
const defaultModel = res.data.find(
|
api.ai.getModels.useQuery(
|
||||||
(model: Model) => model.id === "gpt-4",
|
{
|
||||||
);
|
apiUrl: apiUrl ?? "",
|
||||||
if (defaultModel) {
|
apiKey: apiKey ?? "",
|
||||||
form.setValue("model", defaultModel.id);
|
},
|
||||||
return defaultModel.id;
|
{
|
||||||
}
|
enabled: !!apiUrl && !!apiKey,
|
||||||
} catch (error) {
|
onError: (error) => {
|
||||||
setError("Failed to fetch models. Please check your API URL and Key.");
|
setError(`Failed to fetch models: ${error.message}`);
|
||||||
setModels([]);
|
},
|
||||||
} finally {
|
},
|
||||||
setIsLoadingModels(false);
|
);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const apiUrl = form.watch("apiUrl");
|
const apiUrl = form.watch("apiUrl");
|
||||||
const apiKey = form.watch("apiKey");
|
const apiKey = form.watch("apiKey");
|
||||||
if (apiUrl && apiKey) {
|
if (apiUrl && apiKey) {
|
||||||
form.setValue("model", "");
|
form.setValue("model", "");
|
||||||
fetchModels(apiUrl, apiKey);
|
|
||||||
}
|
}
|
||||||
}, [form.watch("apiUrl"), form.watch("apiKey")]);
|
}, [form.watch("apiUrl"), form.watch("apiKey")]);
|
||||||
|
|
||||||
const onSubmit = async (data: Schema) => {
|
const onSubmit = async (data: Schema) => {
|
||||||
try {
|
try {
|
||||||
console.log("Form data:", data);
|
|
||||||
console.log("Current model value:", form.getValues("model"));
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
...data,
|
...data,
|
||||||
aiId: aiId || "",
|
aiId: aiId || "",
|
||||||
@@ -148,8 +123,9 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
refetch();
|
refetch();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Submit error:", error);
|
toast.error("Failed to save AI settings", {
|
||||||
toast.error("Failed to save AI settings");
|
description: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -232,13 +208,13 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isLoadingModels && (
|
{isLoadingServerModels && (
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Loading models...
|
Loading models...
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoadingModels && models.length > 0 && (
|
{!isLoadingServerModels && models && models.length > 0 && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="model"
|
name="model"
|
||||||
|
|||||||
@@ -139,6 +139,15 @@ export const ValidateServer = ({ serverId }: Props) => {
|
|||||||
: "Not Created"
|
: "Not Created"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Railpack Installed"
|
||||||
|
isEnabled={data?.railpack?.enabled}
|
||||||
|
description={
|
||||||
|
data?.railpack?.enabled
|
||||||
|
? `Installed: ${data?.railpack?.version}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const addServerDomain = z
|
|||||||
.object({
|
.object({
|
||||||
domain: z.string().min(1, { message: "URL is required" }),
|
domain: z.string().min(1, { message: "URL is required" }),
|
||||||
letsEncryptEmail: z.string(),
|
letsEncryptEmail: z.string(),
|
||||||
certificateType: z.enum(["letsencrypt", "none"]),
|
certificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) {
|
if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) {
|
||||||
@@ -193,6 +193,7 @@ export const WebDomain = () => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex w-full justify-end col-span-2">
|
<div className="flex w-full justify-end col-span-2">
|
||||||
<Button isLoading={isLoading} type="submit">
|
<Button isLoading={isLoading} type="submit">
|
||||||
{t("settings.common.save")}
|
{t("settings.common.save")}
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import type { IUpdateData } from "@dokploy/server/index";
|
import type { IUpdateData } from "@dokploy/server/index";
|
||||||
import {
|
import {
|
||||||
@@ -24,9 +30,17 @@ import { UpdateWebServer } from "./update-webserver";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
updateData?: IUpdateData;
|
updateData?: IUpdateData;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
isOpen?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UpdateServer = ({ updateData }: Props) => {
|
export const UpdateServer = ({
|
||||||
|
updateData,
|
||||||
|
children,
|
||||||
|
isOpen: isOpenProp,
|
||||||
|
onOpenChange: onOpenChangeProp,
|
||||||
|
}: Props) => {
|
||||||
const [hasCheckedUpdate, setHasCheckedUpdate] = useState(!!updateData);
|
const [hasCheckedUpdate, setHasCheckedUpdate] = useState(!!updateData);
|
||||||
const [isUpdateAvailable, setIsUpdateAvailable] = useState(
|
const [isUpdateAvailable, setIsUpdateAvailable] = useState(
|
||||||
!!updateData?.updateAvailable,
|
!!updateData?.updateAvailable,
|
||||||
@@ -35,10 +49,10 @@ export const UpdateServer = ({ updateData }: Props) => {
|
|||||||
api.settings.getUpdateData.useMutation();
|
api.settings.getUpdateData.useMutation();
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
const { data: releaseTag } = api.settings.getReleaseTag.useQuery();
|
const { data: releaseTag } = api.settings.getReleaseTag.useQuery();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [latestVersion, setLatestVersion] = useState(
|
const [latestVersion, setLatestVersion] = useState(
|
||||||
updateData?.latestVersion ?? "",
|
updateData?.latestVersion ?? "",
|
||||||
);
|
);
|
||||||
|
const [isOpenInternal, setIsOpenInternal] = useState(false);
|
||||||
|
|
||||||
const handleCheckUpdates = async () => {
|
const handleCheckUpdates = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -65,28 +79,52 @@ export const UpdateServer = ({ updateData }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isOpen = isOpenInternal || isOpenProp;
|
||||||
|
const onOpenChange = (open: boolean) => {
|
||||||
|
setIsOpenInternal(open);
|
||||||
|
onOpenChangeProp?.(open);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
{children ? (
|
||||||
variant={updateData ? "outline" : "secondary"}
|
children
|
||||||
className="gap-2"
|
) : (
|
||||||
>
|
<TooltipProvider delayDuration={0}>
|
||||||
{updateData ? (
|
<Tooltip>
|
||||||
<>
|
<TooltipTrigger asChild>
|
||||||
<span className="flex h-2 w-2">
|
<Button
|
||||||
<span className="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-emerald-400 opacity-75" />
|
variant={updateData ? "outline" : "secondary"}
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
|
size="sm"
|
||||||
</span>
|
onClick={() => onOpenChange?.(true)}
|
||||||
Update available
|
>
|
||||||
</>
|
<Download className="h-4 w-4 flex-shrink-0" />
|
||||||
) : (
|
{updateData ? (
|
||||||
<>
|
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
|
||||||
<Sparkles className="h-4 w-4" />
|
Update Available
|
||||||
Updates
|
</span>
|
||||||
</>
|
) : (
|
||||||
)}
|
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
|
||||||
</Button>
|
Check for updates
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{updateData && (
|
||||||
|
<span className="absolute right-2 flex h-2 w-2 group-data-[collapsible=icon]:hidden">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
|
||||||
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
{updateData && (
|
||||||
|
<TooltipContent side="right" sideOffset={10}>
|
||||||
|
<p>Update Available</p>
|
||||||
|
</TooltipContent>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-lg p-6">
|
<DialogContent className="max-w-lg p-6">
|
||||||
<div className="flex items-center justify-between mb-8">
|
<div className="flex items-center justify-between mb-8">
|
||||||
@@ -217,7 +255,7 @@ export const UpdateServer = ({ updateData }: Props) => {
|
|||||||
|
|
||||||
<div className="space-y-4 flex items-center justify-end">
|
<div className="space-y-4 flex items-center justify-end">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="outline" onClick={() => setIsOpen(false)}>
|
<Button variant="outline" onClick={() => onOpenChange?.(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
{isUpdateAvailable ? (
|
{isUpdateAvailable ? (
|
||||||
|
|||||||
119
apps/dokploy/components/dashboard/shared/rebuild-database.tsx
Normal file
119
apps/dokploy/components/dashboard/shared/rebuild-database.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { DatabaseIcon, AlertTriangle } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
type: "postgres" | "mysql" | "mariadb" | "mongo" | "redis";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RebuildDatabase = ({ id, type }: Props) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
const mutationMap = {
|
||||||
|
postgres: () => api.postgres.rebuild.useMutation(),
|
||||||
|
mysql: () => api.mysql.rebuild.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.rebuild.useMutation(),
|
||||||
|
mongo: () => api.mongo.rebuild.useMutation(),
|
||||||
|
redis: () => api.redis.rebuild.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = mutationMap[type]();
|
||||||
|
|
||||||
|
const handleRebuild = async () => {
|
||||||
|
try {
|
||||||
|
await mutateAsync({
|
||||||
|
postgresId: type === "postgres" ? id : "",
|
||||||
|
mysqlId: type === "mysql" ? id : "",
|
||||||
|
mariadbId: type === "mariadb" ? id : "",
|
||||||
|
mongoId: type === "mongo" ? id : "",
|
||||||
|
redisId: type === "redis" ? id : "",
|
||||||
|
});
|
||||||
|
toast.success("Database rebuilt successfully");
|
||||||
|
await utils.invalidate();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Error rebuilding database", {
|
||||||
|
description: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-background border-destructive/50">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-destructive" />
|
||||||
|
Danger Zone
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h3 className="text-base font-semibold">Rebuild Database</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
This action will completely reset your database to its initial
|
||||||
|
state. All data, tables, and configurations will be removed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full border-destructive/50 hover:bg-destructive/10 hover:text-destructive text-destructive"
|
||||||
|
>
|
||||||
|
<DatabaseIcon className="mr-2 h-4 w-4" />
|
||||||
|
Rebuild Database
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-destructive" />
|
||||||
|
Are you absolutely sure?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription className="space-y-2">
|
||||||
|
<p>This action will:</p>
|
||||||
|
<ul className="list-disc list-inside space-y-1">
|
||||||
|
<li>Stop the current database service</li>
|
||||||
|
<li>Delete all existing data and volumes</li>
|
||||||
|
<li>Reset to the default configuration</li>
|
||||||
|
<li>Restart the service with a clean state</li>
|
||||||
|
</ul>
|
||||||
|
<p className="font-medium text-destructive mt-4">
|
||||||
|
This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleRebuild}
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Button isLoading={isLoading} type="submit">
|
||||||
|
Yes, rebuild database
|
||||||
|
</Button>
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
|
||||||
|
import { ShowResources } from "@/components/dashboard/application/advanced/show-resources";
|
||||||
|
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
||||||
|
import { RebuildDatabase } from "./rebuild-database";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
type: "postgres" | "mysql" | "mariadb" | "mongo" | "redis";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowDatabaseAdvancedSettings = ({ id, type }: Props) => {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col gap-5">
|
||||||
|
<ShowCustomCommand id={id} type={type} />
|
||||||
|
<ShowVolumes id={id} type={type} />
|
||||||
|
<ShowResources id={id} type={type} />
|
||||||
|
<RebuildDatabase id={id} type={type} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -37,8 +37,6 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
@@ -302,6 +300,7 @@ const MENU: Menu = {
|
|||||||
icon: BotIcon,
|
icon: BotIcon,
|
||||||
url: "/dashboard/settings/ai",
|
url: "/dashboard/settings/ai",
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
|
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -497,7 +496,6 @@ function SidebarLogo() {
|
|||||||
const { state } = useSidebar();
|
const { state } = useSidebar();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: user } = api.user.get.useQuery();
|
const { data: user } = api.user.get.useQuery();
|
||||||
// const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
|
||||||
const { data: session } = authClient.useSession();
|
const { data: session } = authClient.useSession();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -773,6 +771,7 @@ export default function Page({ children }: Props) {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const _currentPath = router.pathname;
|
const _currentPath = router.pathname;
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
|
|
||||||
const includesProjects = pathname?.includes("/dashboard/project");
|
const includesProjects = pathname?.includes("/dashboard/project");
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
@@ -1016,21 +1015,29 @@ export default function Page({ children }: Props) {
|
|||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
{!isCloud && auth?.role === "owner" && (
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton asChild>
|
|
||||||
<UpdateServerButton />
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
)}
|
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<SidebarMenu>
|
<SidebarMenu className="flex flex-col gap-2">
|
||||||
|
{!isCloud && auth?.role === "owner" && (
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<UpdateServerButton />
|
||||||
|
</SidebarMenuItem>
|
||||||
|
)}
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<UserNav />
|
<UserNav />
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
{dokployVersion && (
|
||||||
|
<>
|
||||||
|
<div className="px-3 text-xs text-muted-foreground text-center group-data-[collapsible=icon]:hidden">
|
||||||
|
Version {dokployVersion}
|
||||||
|
</div>
|
||||||
|
<div className="hidden text-xs text-muted-foreground text-center group-data-[collapsible=icon]:block">
|
||||||
|
{dokployVersion}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
@@ -1054,10 +1061,6 @@ export default function Page({ children }: Props) {
|
|||||||
</Link>
|
</Link>
|
||||||
</BreadcrumbLink>
|
</BreadcrumbLink>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator className="block" />
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbPage>{activeItem?.title}</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,14 @@ import type { IUpdateData } from "@dokploy/server/index";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import UpdateServer from "../dashboard/settings/web-server/update-server";
|
import UpdateServer from "../dashboard/settings/web-server/update-server";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { Download } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "../ui/tooltip";
|
||||||
const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
||||||
|
|
||||||
export const UpdateServerButton = () => {
|
export const UpdateServerButton = () => {
|
||||||
@@ -15,6 +22,7 @@ export const UpdateServerButton = () => {
|
|||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { mutateAsync: getUpdateData } =
|
const { mutateAsync: getUpdateData } =
|
||||||
api.settings.getUpdateData.useMutation();
|
api.settings.getUpdateData.useMutation();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null);
|
const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null);
|
||||||
|
|
||||||
@@ -69,11 +77,47 @@ export const UpdateServerButton = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return updateData.updateAvailable ? (
|
||||||
updateData.updateAvailable && (
|
<div className="border-t pt-4">
|
||||||
<div>
|
<UpdateServer
|
||||||
<UpdateServer updateData={updateData} />
|
updateData={updateData}
|
||||||
</div>
|
isOpen={isOpen}
|
||||||
)
|
onOpenChange={setIsOpen}
|
||||||
);
|
>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={updateData ? "outline" : "secondary"}
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 flex-shrink-0" />
|
||||||
|
{updateData ? (
|
||||||
|
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
|
||||||
|
Update Available
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
|
||||||
|
Check for updates
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{updateData && (
|
||||||
|
<span className="absolute right-2 flex h-2 w-2 group-data-[collapsible=icon]:hidden">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
|
||||||
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
{updateData && (
|
||||||
|
<TooltipContent side="right" sideOffset={10}>
|
||||||
|
<p>Update Available</p>
|
||||||
|
</TooltipContent>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</UpdateServer>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -72,6 +72,14 @@ export const UserNav = () => {
|
|||||||
</div>
|
</div>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
router.push("/dashboard/settings/profile");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Profile
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -126,14 +134,6 @@ export const UserNav = () => {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuItem
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
router.push("/dashboard/settings/profile");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Profile
|
|
||||||
</DropdownMenuItem>
|
|
||||||
{data?.role === "owner" && (
|
{data?.role === "owner" && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</BreadcrumbLink>
|
</BreadcrumbLink>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator className="block" />
|
{_index + 1 < list.length && <BreadcrumbSeparator className="block" />}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const CodeEditor = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{props.disabled && (
|
{props.disabled && (
|
||||||
<div className="absolute top-0 rounded-md left-0 w-full h-full flex items-center justify-center z-[10] [background:var(--overlay)]" />
|
<div className="absolute top-0 rounded-md left-0 w-full h-full flex items-center justify-center z-[10] [background:var(--overlay)] h-full" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
68
apps/dokploy/components/ui/calendar.tsx
Normal file
68
apps/dokploy/components/ui/calendar.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import type * as React from "react";
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { DayPicker } from "react-day-picker";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||||
|
|
||||||
|
function Calendar({
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
showOutsideDays = true,
|
||||||
|
...props
|
||||||
|
}: CalendarProps) {
|
||||||
|
return (
|
||||||
|
<DayPicker
|
||||||
|
showOutsideDays={showOutsideDays}
|
||||||
|
className={cn("p-3", className)}
|
||||||
|
classNames={{
|
||||||
|
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||||
|
month: "space-y-4",
|
||||||
|
caption: "flex justify-center pt-1 relative items-center",
|
||||||
|
caption_label: "text-sm font-medium",
|
||||||
|
nav: "space-x-1 flex items-center",
|
||||||
|
nav_button: cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||||
|
),
|
||||||
|
nav_button_previous: "absolute left-1",
|
||||||
|
nav_button_next: "absolute right-1",
|
||||||
|
table: "w-full border-collapse space-y-1",
|
||||||
|
head_row: "flex",
|
||||||
|
head_cell:
|
||||||
|
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
||||||
|
row: "flex w-full mt-2",
|
||||||
|
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
||||||
|
day: cn(
|
||||||
|
buttonVariants({ variant: "ghost" }),
|
||||||
|
"h-9 w-9 p-0 font-normal aria-selected:opacity-100",
|
||||||
|
),
|
||||||
|
day_range_end: "day-range-end",
|
||||||
|
day_selected:
|
||||||
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||||
|
day_today: "bg-accent text-accent-foreground",
|
||||||
|
day_outside:
|
||||||
|
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
||||||
|
day_disabled: "text-muted-foreground opacity-50",
|
||||||
|
day_range_middle:
|
||||||
|
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||||
|
day_hidden: "invisible",
|
||||||
|
...classNames,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
IconLeft: ({ className, ...props }) => (
|
||||||
|
<ChevronLeft className={cn("h-4 w-4", className)} {...props} />
|
||||||
|
),
|
||||||
|
IconRight: ({ className, ...props }) => (
|
||||||
|
<ChevronRight className={cn("h-4 w-4", className)} {...props} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Calendar.displayName = "Calendar";
|
||||||
|
|
||||||
|
export { Calendar };
|
||||||
@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SwitchPrimitives.Root
|
<SwitchPrimitives.Root
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted-foreground/80",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0069_legal_bill_hollister.sql
Normal file
1
apps/dokploy/drizzle/0069_legal_bill_hollister.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TYPE "public"."buildType" ADD VALUE 'railpack';
|
||||||
1
apps/dokploy/drizzle/0070_useful_serpent_society.sql
Normal file
1
apps/dokploy/drizzle/0070_useful_serpent_society.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "destination" ADD COLUMN "createdAt" timestamp DEFAULT now() NOT NULL;
|
||||||
1
apps/dokploy/drizzle/0071_flaky_black_queen.sql
Normal file
1
apps/dokploy/drizzle/0071_flaky_black_queen.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user_temp" ADD COLUMN "logCleanupCron" text;
|
||||||
2
apps/dokploy/drizzle/0072_green_susan_delgado.sql
Normal file
2
apps/dokploy/drizzle/0072_green_susan_delgado.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TYPE "public"."certificateType" ADD VALUE 'custom';--> statement-breakpoint
|
||||||
|
ALTER TABLE "domain" ADD COLUMN "customCertResolver" text;--> statement-breakpoint
|
||||||
1
apps/dokploy/drizzle/0073_hot_domino.sql
Normal file
1
apps/dokploy/drizzle/0073_hot_domino.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "previewCertificateProvider" text;
|
||||||
1
apps/dokploy/drizzle/0074_black_quasar.sql
Normal file
1
apps/dokploy/drizzle/0074_black_quasar.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "application" RENAME COLUMN "previewCertificateProvider" TO "previewCustomCertResolver";
|
||||||
1
apps/dokploy/drizzle/0075_young_typhoid_mary.sql
Normal file
1
apps/dokploy/drizzle/0075_young_typhoid_mary.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "watchPaths" text[];
|
||||||
1
apps/dokploy/drizzle/0076_young_sharon_ventura.sql
Normal file
1
apps/dokploy/drizzle/0076_young_sharon_ventura.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "compose" ADD COLUMN "watchPaths" text[];
|
||||||
5119
apps/dokploy/drizzle/meta/0069_snapshot.json
Normal file
5119
apps/dokploy/drizzle/meta/0069_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5126
apps/dokploy/drizzle/meta/0070_snapshot.json
Normal file
5126
apps/dokploy/drizzle/meta/0070_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5132
apps/dokploy/drizzle/meta/0071_snapshot.json
Normal file
5132
apps/dokploy/drizzle/meta/0071_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5132
apps/dokploy/drizzle/meta/0072_snapshot.json
Normal file
5132
apps/dokploy/drizzle/meta/0072_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5138
apps/dokploy/drizzle/meta/0073_snapshot.json
Normal file
5138
apps/dokploy/drizzle/meta/0073_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5138
apps/dokploy/drizzle/meta/0074_snapshot.json
Normal file
5138
apps/dokploy/drizzle/meta/0074_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5144
apps/dokploy/drizzle/meta/0075_snapshot.json
Normal file
5144
apps/dokploy/drizzle/meta/0075_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5150
apps/dokploy/drizzle/meta/0076_snapshot.json
Normal file
5150
apps/dokploy/drizzle/meta/0076_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -484,6 +484,62 @@
|
|||||||
"when": 1740897756774,
|
"when": 1740897756774,
|
||||||
"tag": "0068_complex_rhino",
|
"tag": "0068_complex_rhino",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 69,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741152916611,
|
||||||
|
"tag": "0069_legal_bill_hollister",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 70,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741322697251,
|
||||||
|
"tag": "0070_useful_serpent_society",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 71,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741460060541,
|
||||||
|
"tag": "0071_flaky_black_queen",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 72,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741487009559,
|
||||||
|
"tag": "0072_green_susan_delgado",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 73,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741489681190,
|
||||||
|
"tag": "0073_hot_domino",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 74,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741490064139,
|
||||||
|
"tag": "0074_black_quasar",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 75,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741491527516,
|
||||||
|
"tag": "0075_young_typhoid_mary",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 76,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741493754270,
|
||||||
|
"tag": "0076_young_sharon_ventura",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.18.4",
|
"version": "v0.19.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
"test": "vitest --config __test__/vitest.config.ts"
|
"test": "vitest --config __test__/vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"micromatch": "4.0.8",
|
||||||
"ai": "^4.0.23",
|
"ai": "^4.0.23",
|
||||||
"@ai-sdk/anthropic": "^1.0.6",
|
"@ai-sdk/anthropic": "^1.0.6",
|
||||||
"@ai-sdk/azure": "^1.0.15",
|
"@ai-sdk/azure": "^1.0.15",
|
||||||
@@ -154,6 +155,7 @@
|
|||||||
"@tailwindcss/typography": "0.5.16"
|
"@tailwindcss/typography": "0.5.16"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/micromatch": "4.0.9",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/nodemailer": "^6.4.15",
|
"@types/nodemailer": "^6.4.15",
|
||||||
"@types/node-os-utils": "1.3.4",
|
"@types/node-os-utils": "1.3.4",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { applications } from "@/server/db/schema";
|
|||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import { IS_CLOUD } from "@dokploy/server";
|
import { IS_CLOUD, shouldDeploy } from "@dokploy/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ export default async function handler(
|
|||||||
where: eq(applications.refreshToken, refreshToken as string),
|
where: eq(applications.refreshToken, refreshToken as string),
|
||||||
with: {
|
with: {
|
||||||
project: true,
|
project: true,
|
||||||
|
bitbucket: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,6 +58,20 @@ export default async function handler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (sourceType === "github") {
|
} else if (sourceType === "github") {
|
||||||
|
const normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldDeployPaths = shouldDeploy(
|
||||||
|
application.watchPaths,
|
||||||
|
normalizedCommits,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDeployPaths) {
|
||||||
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
if (!branchName || branchName !== application.branch) {
|
if (!branchName || branchName !== application.branch) {
|
||||||
res.status(301).json({ message: "Branch Not Match" });
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
@@ -64,22 +79,55 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
} else if (sourceType === "git") {
|
} else if (sourceType === "git") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
|
||||||
if (!branchName || branchName !== application.customGitBranch) {
|
if (!branchName || branchName !== application.customGitBranch) {
|
||||||
res.status(301).json({ message: "Branch Not Match" });
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (sourceType === "gitlab") {
|
} else if (sourceType === "gitlab") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
|
||||||
|
const normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldDeployPaths = shouldDeploy(
|
||||||
|
application.watchPaths,
|
||||||
|
normalizedCommits,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDeployPaths) {
|
||||||
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!branchName || branchName !== application.gitlabBranch) {
|
if (!branchName || branchName !== application.gitlabBranch) {
|
||||||
res.status(301).json({ message: "Branch Not Match" });
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (sourceType === "bitbucket") {
|
} else if (sourceType === "bitbucket") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
|
||||||
if (!branchName || branchName !== application.bitbucketBranch) {
|
if (!branchName || branchName !== application.bitbucketBranch) {
|
||||||
res.status(301).json({ message: "Branch Not Match" });
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const commitedPaths = await extractCommitedPaths(
|
||||||
|
req.body,
|
||||||
|
application.bitbucketOwner,
|
||||||
|
application.bitbucket?.appPassword || "",
|
||||||
|
application.bitbucketRepository || "",
|
||||||
|
);
|
||||||
|
const shouldDeployPaths = shouldDeploy(
|
||||||
|
application.watchPaths,
|
||||||
|
commitedPaths,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDeployPaths) {
|
||||||
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -231,3 +279,42 @@ export const extractBranchName = (headers: any, body: any) => {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const extractCommitedPaths = async (
|
||||||
|
body: any,
|
||||||
|
bitbucketUsername: string | null,
|
||||||
|
bitbucketAppPassword: string | null,
|
||||||
|
repository: string | null,
|
||||||
|
) => {
|
||||||
|
const changes = body.push?.changes || [];
|
||||||
|
|
||||||
|
const commitHashes = changes
|
||||||
|
.map((change: any) => change.new?.target?.hash)
|
||||||
|
.filter(Boolean);
|
||||||
|
const commitedPaths: string[] = [];
|
||||||
|
for (const commit of commitHashes) {
|
||||||
|
const url = `https://api.bitbucket.org/2.0/repositories/${bitbucketUsername}/${repository}/diffstat/${commit}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${Buffer.from(`${bitbucketUsername}:${bitbucketAppPassword}`).toString("base64")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
for (const value of data.values) {
|
||||||
|
commitedPaths.push(value.new?.path);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error fetching Bitbucket diffstat for commit ${commit}:`,
|
||||||
|
error instanceof Error ? error.message : "Unknown error",
|
||||||
|
);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return commitedPaths;
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { compose } from "@/server/db/schema";
|
|||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import { IS_CLOUD } from "@dokploy/server";
|
import { IS_CLOUD, shouldDeploy } from "@dokploy/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import {
|
import {
|
||||||
extractBranchName,
|
extractBranchName,
|
||||||
|
extractCommitedPaths,
|
||||||
extractCommitMessage,
|
extractCommitMessage,
|
||||||
extractHash,
|
extractHash,
|
||||||
} from "../[refreshToken]";
|
} from "../[refreshToken]";
|
||||||
@@ -26,6 +27,7 @@ export default async function handler(
|
|||||||
where: eq(compose.refreshToken, refreshToken as string),
|
where: eq(compose.refreshToken, refreshToken as string),
|
||||||
with: {
|
with: {
|
||||||
project: true,
|
project: true,
|
||||||
|
bitbucket: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,16 +48,71 @@ export default async function handler(
|
|||||||
|
|
||||||
if (sourceType === "github") {
|
if (sourceType === "github") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
const normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldDeployPaths = shouldDeploy(
|
||||||
|
composeResult.watchPaths,
|
||||||
|
normalizedCommits,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDeployPaths) {
|
||||||
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!branchName || branchName !== composeResult.branch) {
|
if (!branchName || branchName !== composeResult.branch) {
|
||||||
res.status(301).json({ message: "Branch Not Match" });
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else if (sourceType === "gitlab") {
|
||||||
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
const normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldDeployPaths = shouldDeploy(
|
||||||
|
composeResult.watchPaths,
|
||||||
|
normalizedCommits,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDeployPaths) {
|
||||||
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!branchName || branchName !== composeResult.gitlabBranch) {
|
||||||
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (sourceType === "bitbucket") {
|
||||||
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
if (!branchName || branchName !== composeResult.bitbucketBranch) {
|
||||||
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else if (sourceType === "git") {
|
} else if (sourceType === "git") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
if (!branchName || branchName !== composeResult.customGitBranch) {
|
if (!branchName || branchName !== composeResult.customGitBranch) {
|
||||||
res.status(301).json({ message: "Branch Not Match" });
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const commitedPaths = await extractCommitedPaths(
|
||||||
|
req.body,
|
||||||
|
composeResult.bitbucketOwner,
|
||||||
|
composeResult.bitbucket?.appPassword || "",
|
||||||
|
composeResult.bitbucketRepository || "",
|
||||||
|
);
|
||||||
|
const shouldDeployPaths = shouldDeploy(
|
||||||
|
composeResult.watchPaths,
|
||||||
|
commitedPaths,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDeployPaths) {
|
||||||
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
findPreviewDeploymentByApplicationId,
|
findPreviewDeploymentByApplicationId,
|
||||||
findPreviewDeploymentsByPullRequestId,
|
findPreviewDeploymentsByPullRequestId,
|
||||||
removePreviewDeployment,
|
removePreviewDeployment,
|
||||||
|
shouldDeploy,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { Webhooks } from "@octokit/webhooks";
|
import { Webhooks } from "@octokit/webhooks";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
@@ -95,6 +96,9 @@ export default async function handler(
|
|||||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||||
const deploymentHash = extractHash(req.headers, req.body);
|
const deploymentHash = extractHash(req.headers, req.body);
|
||||||
const owner = githubBody?.repository?.owner?.name;
|
const owner = githubBody?.repository?.owner?.name;
|
||||||
|
const normalizedCommits = githubBody?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
|
||||||
const apps = await db.query.applications.findMany({
|
const apps = await db.query.applications.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
@@ -116,6 +120,15 @@ export default async function handler(
|
|||||||
server: !!app.serverId,
|
server: !!app.serverId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const shouldDeployPaths = shouldDeploy(
|
||||||
|
app.watchPaths,
|
||||||
|
normalizedCommits,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDeployPaths) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (IS_CLOUD && app.serverId) {
|
if (IS_CLOUD && app.serverId) {
|
||||||
jobData.serverId = app.serverId;
|
jobData.serverId = app.serverId;
|
||||||
await deploy(jobData);
|
await deploy(jobData);
|
||||||
@@ -151,6 +164,14 @@ export default async function handler(
|
|||||||
server: !!composeApp.serverId,
|
server: !!composeApp.serverId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const shouldDeployPaths = shouldDeploy(
|
||||||
|
composeApp.watchPaths,
|
||||||
|
normalizedCommits,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDeployPaths) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (IS_CLOUD && composeApp.serverId) {
|
if (IS_CLOUD && composeApp.serverId) {
|
||||||
jobData.serverId = composeApp.serverId;
|
jobData.serverId = composeApp.serverId;
|
||||||
await deploy(jobData);
|
await deploy(jobData);
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ import {
|
|||||||
PlusIcon,
|
PlusIcon,
|
||||||
Search,
|
Search,
|
||||||
X,
|
X,
|
||||||
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
@@ -72,9 +73,25 @@ import type {
|
|||||||
} from "next";
|
} from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { type ReactElement, useMemo, useState } from "react";
|
import { type ReactElement, useMemo, useState, useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
export type Services = {
|
export type Services = {
|
||||||
appName: string;
|
appName: string;
|
||||||
@@ -203,10 +220,47 @@ const Project = (
|
|||||||
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
||||||
const { projectId } = props;
|
const { projectId } = props;
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const [sortBy, setSortBy] = useState<string>(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
return localStorage.getItem("servicesSort") || "createdAt-desc";
|
||||||
|
}
|
||||||
|
return "createdAt-desc";
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem("servicesSort", sortBy);
|
||||||
|
}, [sortBy]);
|
||||||
|
|
||||||
|
const sortServices = (services: Services[]) => {
|
||||||
|
const [field, direction] = sortBy.split("-");
|
||||||
|
return [...services].sort((a, b) => {
|
||||||
|
let comparison = 0;
|
||||||
|
switch (field) {
|
||||||
|
case "name":
|
||||||
|
comparison = a.name.localeCompare(b.name);
|
||||||
|
break;
|
||||||
|
case "type":
|
||||||
|
comparison = a.type.localeCompare(b.type);
|
||||||
|
break;
|
||||||
|
case "createdAt":
|
||||||
|
comparison =
|
||||||
|
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
comparison = 0;
|
||||||
|
}
|
||||||
|
return direction === "asc" ? comparison : -comparison;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const { data, isLoading, refetch } = api.project.one.useQuery({ projectId });
|
const { data, isLoading, refetch } = api.project.one.useQuery({ projectId });
|
||||||
|
const { data: allProjects } = api.project.all.useQuery();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [isMoveDialogOpen, setIsMoveDialogOpen] = useState(false);
|
||||||
|
const [selectedTargetProject, setSelectedTargetProject] =
|
||||||
|
useState<string>("");
|
||||||
|
|
||||||
const emptyServices =
|
const emptyServices =
|
||||||
data?.mariadb?.length === 0 &&
|
data?.mariadb?.length === 0 &&
|
||||||
data?.mongo?.length === 0 &&
|
data?.mongo?.length === 0 &&
|
||||||
@@ -254,6 +308,38 @@ const Project = (
|
|||||||
const composeActions = {
|
const composeActions = {
|
||||||
start: api.compose.start.useMutation(),
|
start: api.compose.start.useMutation(),
|
||||||
stop: api.compose.stop.useMutation(),
|
stop: api.compose.stop.useMutation(),
|
||||||
|
move: api.compose.move.useMutation(),
|
||||||
|
delete: api.compose.delete.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const applicationActions = {
|
||||||
|
move: api.application.move.useMutation(),
|
||||||
|
delete: api.application.delete.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const postgresActions = {
|
||||||
|
move: api.postgres.move.useMutation(),
|
||||||
|
delete: api.postgres.remove.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mysqlActions = {
|
||||||
|
move: api.mysql.move.useMutation(),
|
||||||
|
delete: api.mysql.remove.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mariadbActions = {
|
||||||
|
move: api.mariadb.move.useMutation(),
|
||||||
|
delete: api.mariadb.remove.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const redisActions = {
|
||||||
|
move: api.redis.move.useMutation(),
|
||||||
|
delete: api.redis.remove.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mongoActions = {
|
||||||
|
move: api.mongo.move.useMutation(),
|
||||||
|
delete: api.mongo.remove.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkStart = async () => {
|
const handleBulkStart = async () => {
|
||||||
@@ -296,9 +382,145 @@ const Project = (
|
|||||||
setIsBulkActionLoading(false);
|
setIsBulkActionLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBulkMove = async () => {
|
||||||
|
if (!selectedTargetProject) {
|
||||||
|
toast.error("Please select a target project");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let success = 0;
|
||||||
|
setIsBulkActionLoading(true);
|
||||||
|
for (const serviceId of selectedServices) {
|
||||||
|
try {
|
||||||
|
const service = filteredServices.find((s) => s.id === serviceId);
|
||||||
|
if (!service) continue;
|
||||||
|
|
||||||
|
switch (service.type) {
|
||||||
|
case "application":
|
||||||
|
await applicationActions.move.mutateAsync({
|
||||||
|
applicationId: serviceId,
|
||||||
|
targetProjectId: selectedTargetProject,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "compose":
|
||||||
|
await composeActions.move.mutateAsync({
|
||||||
|
composeId: serviceId,
|
||||||
|
targetProjectId: selectedTargetProject,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "postgres":
|
||||||
|
await postgresActions.move.mutateAsync({
|
||||||
|
postgresId: serviceId,
|
||||||
|
targetProjectId: selectedTargetProject,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "mysql":
|
||||||
|
await mysqlActions.move.mutateAsync({
|
||||||
|
mysqlId: serviceId,
|
||||||
|
targetProjectId: selectedTargetProject,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "mariadb":
|
||||||
|
await mariadbActions.move.mutateAsync({
|
||||||
|
mariadbId: serviceId,
|
||||||
|
targetProjectId: selectedTargetProject,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "redis":
|
||||||
|
await redisActions.move.mutateAsync({
|
||||||
|
redisId: serviceId,
|
||||||
|
targetProjectId: selectedTargetProject,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "mongo":
|
||||||
|
await mongoActions.move.mutateAsync({
|
||||||
|
mongoId: serviceId,
|
||||||
|
targetProjectId: selectedTargetProject,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
success++;
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
`Error moving service ${serviceId}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (success > 0) {
|
||||||
|
toast.success(`${success} services moved successfully`);
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
setSelectedServices([]);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
setIsMoveDialogOpen(false);
|
||||||
|
setIsBulkActionLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkDelete = async () => {
|
||||||
|
let success = 0;
|
||||||
|
setIsBulkActionLoading(true);
|
||||||
|
for (const serviceId of selectedServices) {
|
||||||
|
try {
|
||||||
|
const service = filteredServices.find((s) => s.id === serviceId);
|
||||||
|
if (!service) continue;
|
||||||
|
|
||||||
|
switch (service.type) {
|
||||||
|
case "application":
|
||||||
|
await applicationActions.delete.mutateAsync({
|
||||||
|
applicationId: serviceId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "compose":
|
||||||
|
await composeActions.delete.mutateAsync({
|
||||||
|
composeId: serviceId,
|
||||||
|
deleteVolumes: false,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "postgres":
|
||||||
|
await postgresActions.delete.mutateAsync({
|
||||||
|
postgresId: serviceId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "mysql":
|
||||||
|
await mysqlActions.delete.mutateAsync({
|
||||||
|
mysqlId: serviceId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "mariadb":
|
||||||
|
await mariadbActions.delete.mutateAsync({
|
||||||
|
mariadbId: serviceId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "redis":
|
||||||
|
await redisActions.delete.mutateAsync({
|
||||||
|
redisId: serviceId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "mongo":
|
||||||
|
await mongoActions.delete.mutateAsync({
|
||||||
|
mongoId: serviceId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
success++;
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
`Error deleting service ${serviceId}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (success > 0) {
|
||||||
|
toast.success(`${success} services deleted successfully`);
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
setSelectedServices([]);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
setIsBulkActionLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
const filteredServices = useMemo(() => {
|
const filteredServices = useMemo(() => {
|
||||||
if (!applications) return [];
|
if (!applications) return [];
|
||||||
return applications.filter(
|
const filtered = applications.filter(
|
||||||
(service) =>
|
(service) =>
|
||||||
(service.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
(service.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
service.description
|
service.description
|
||||||
@@ -306,7 +528,8 @@ const Project = (
|
|||||||
.includes(searchQuery.toLowerCase())) &&
|
.includes(searchQuery.toLowerCase())) &&
|
||||||
(selectedTypes.length === 0 || selectedTypes.includes(service.type)),
|
(selectedTypes.length === 0 || selectedTypes.includes(service.type)),
|
||||||
);
|
);
|
||||||
}, [applications, searchQuery, selectedTypes]);
|
return sortServices(filtered);
|
||||||
|
}, [applications, searchQuery, selectedTypes, sortBy]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -380,7 +603,7 @@ const Project = (
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -445,11 +668,107 @@ const Project = (
|
|||||||
Stop
|
Stop
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.canDeleteServices) && (
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Services"
|
||||||
|
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
|
||||||
|
type="destructive"
|
||||||
|
onClick={handleBulkDelete}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full justify-start text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={isMoveDialogOpen}
|
||||||
|
onOpenChange={setIsMoveDialogOpen}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full justify-start"
|
||||||
|
>
|
||||||
|
<FolderInput className="mr-2 h-4 w-4" />
|
||||||
|
Move
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Move Services</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Select the target project to move{" "}
|
||||||
|
{selectedServices.length} services
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{allProjects?.filter(
|
||||||
|
(p) => p.projectId !== projectId,
|
||||||
|
).length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-2 py-4">
|
||||||
|
<FolderInput className="h-8 w-8 text-muted-foreground" />
|
||||||
|
<p className="text-sm text-muted-foreground text-center">
|
||||||
|
No other projects available. Create a new
|
||||||
|
project first to move services.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Select
|
||||||
|
value={selectedTargetProject}
|
||||||
|
onValueChange={setSelectedTargetProject}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select target project" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{allProjects
|
||||||
|
?.filter(
|
||||||
|
(p) => p.projectId !== projectId,
|
||||||
|
)
|
||||||
|
.map((project) => (
|
||||||
|
<SelectItem
|
||||||
|
key={project.projectId}
|
||||||
|
value={project.projectId}
|
||||||
|
>
|
||||||
|
{project.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsMoveDialogOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleBulkMove}
|
||||||
|
isLoading={isBulkActionLoading}
|
||||||
|
disabled={
|
||||||
|
allProjects?.filter(
|
||||||
|
(p) => p.projectId !== projectId,
|
||||||
|
).length === 0
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Move Services
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 sm:flex-row sm:gap-4 sm:items-center">
|
<div className="flex flex-col gap-2 lg:flex-row lg:gap-4 lg:items-center">
|
||||||
<div className="w-full relative">
|
<div className="w-full relative">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Filter services..."
|
placeholder="Filter services..."
|
||||||
@@ -459,6 +778,23 @@ const Project = (
|
|||||||
/>
|
/>
|
||||||
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
|
<Select value={sortBy} onValueChange={setSortBy}>
|
||||||
|
<SelectTrigger className="lg:w-[280px]">
|
||||||
|
<SelectValue placeholder="Sort by..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="createdAt-desc">
|
||||||
|
Newest first
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="createdAt-asc">
|
||||||
|
Oldest first
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
|
||||||
|
<SelectItem value="name-desc">Name (Z-A)</SelectItem>
|
||||||
|
<SelectItem value="type-asc">Type (A-Z)</SelectItem>
|
||||||
|
<SelectItem value="type-desc">Type (Z-A)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
<Popover
|
<Popover
|
||||||
open={openCombobox}
|
open={openCombobox}
|
||||||
onOpenChange={setOpenCombobox}
|
onOpenChange={setOpenCombobox}
|
||||||
|
|||||||
@@ -228,15 +228,15 @@ const Service = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
|
||||||
<TabsTrigger value="preview-deployments">
|
<TabsTrigger value="preview-deployments">
|
||||||
Preview Deployments
|
Preview Deployments
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -224,12 +224,12 @@ const Service = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { ShowResources } from "@/components/dashboard/application/advanced/show-resources";
|
|
||||||
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
|
||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
@@ -10,7 +8,7 @@ import { ShowInternalMariadbCredentials } from "@/components/dashboard/mariadb/g
|
|||||||
import { UpdateMariadb } from "@/components/dashboard/mariadb/update-mariadb";
|
import { UpdateMariadb } from "@/components/dashboard/mariadb/update-mariadb";
|
||||||
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
||||||
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
||||||
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
|
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||||
import { MariadbIcon } from "@/components/icons/data-tools-icons";
|
import { MariadbIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
@@ -197,11 +195,11 @@ const Mariadb = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,11 +276,10 @@ const Mariadb = (
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex w-full flex-col gap-5">
|
<ShowDatabaseAdvancedSettings
|
||||||
<ShowCustomCommand id={mariadbId} type="mariadb" />
|
id={mariadbId}
|
||||||
<ShowVolumes id={mariadbId} type="mariadb" />
|
type="mariadb"
|
||||||
<ShowResources id={mariadbId} type="mariadb" />
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { ShowResources } from "@/components/dashboard/application/advanced/show-resources";
|
|
||||||
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
|
||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
@@ -10,7 +8,7 @@ import { ShowInternalMongoCredentials } from "@/components/dashboard/mongo/gener
|
|||||||
import { UpdateMongo } from "@/components/dashboard/mongo/update-mongo";
|
import { UpdateMongo } from "@/components/dashboard/mongo/update-mongo";
|
||||||
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
||||||
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
||||||
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
|
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||||
import { MongodbIcon } from "@/components/icons/data-tools-icons";
|
import { MongodbIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
@@ -198,11 +196,11 @@ const Mongo = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
@@ -279,11 +277,7 @@ const Mongo = (
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<ShowDatabaseAdvancedSettings id={mongoId} type="mongo" />
|
||||||
<ShowCustomCommand id={mongoId} type="mongo" />
|
|
||||||
<ShowVolumes id={mongoId} type="mongo" />
|
|
||||||
<ShowResources id={mongoId} type="mongo" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { ShowResources } from "@/components/dashboard/application/advanced/show-resources";
|
|
||||||
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
|
||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
@@ -10,7 +8,7 @@ import { ShowExternalMysqlCredentials } from "@/components/dashboard/mysql/gener
|
|||||||
import { ShowGeneralMysql } from "@/components/dashboard/mysql/general/show-general-mysql";
|
import { ShowGeneralMysql } from "@/components/dashboard/mysql/general/show-general-mysql";
|
||||||
import { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/general/show-internal-mysql-credentials";
|
import { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/general/show-internal-mysql-credentials";
|
||||||
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
|
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
|
||||||
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
|
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||||
import { MysqlIcon } from "@/components/icons/data-tools-icons";
|
import { MysqlIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
@@ -200,13 +198,13 @@ const MySql = (
|
|||||||
<TabsTrigger value="environment">
|
<TabsTrigger value="environment">
|
||||||
Environment
|
Environment
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">
|
<TabsTrigger value="monitoring">
|
||||||
Monitoring
|
Monitoring
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
@@ -236,33 +234,9 @@ const MySql = (
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* {monitoring?.enabledFeatures && (
|
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
|
||||||
<Label className="text-muted-foreground">
|
|
||||||
Change Monitoring
|
|
||||||
</Label>
|
|
||||||
<Switch
|
|
||||||
checked={toggleMonitoring}
|
|
||||||
onCheckedChange={setToggleMonitoring}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{toggleMonitoring ? (
|
|
||||||
<ContainerPaidMonitoring
|
|
||||||
appName={data?.appName || ""}
|
|
||||||
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
|
|
||||||
token={
|
|
||||||
monitoring?.metricsConfig?.server?.token || ""
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div> */}
|
|
||||||
<ContainerFreeMonitoring
|
<ContainerFreeMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
/>
|
/>
|
||||||
{/* </div> */}
|
|
||||||
{/* )} */}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -283,11 +257,10 @@ const MySql = (
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex w-full flex-col gap-5">
|
<ShowDatabaseAdvancedSettings
|
||||||
<ShowCustomCommand id={mysqlId} type="mysql" />
|
id={mysqlId}
|
||||||
<ShowVolumes id={mysqlId} type="mysql" />
|
type="mysql"
|
||||||
<ShowResources id={mysqlId} type="mysql" />
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { ShowResources } from "@/components/dashboard/application/advanced/show-resources";
|
|
||||||
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
|
||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||||
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
||||||
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
||||||
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
|
|
||||||
import { ShowExternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-external-postgres-credentials";
|
import { ShowExternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-external-postgres-credentials";
|
||||||
import { ShowGeneralPostgres } from "@/components/dashboard/postgres/general/show-general-postgres";
|
import { ShowGeneralPostgres } from "@/components/dashboard/postgres/general/show-general-postgres";
|
||||||
import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-internal-postgres-credentials";
|
import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-internal-postgres-credentials";
|
||||||
@@ -15,6 +12,7 @@ import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
|
|||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -197,11 +195,11 @@ const Postgresql = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
@@ -235,33 +233,9 @@ const Postgresql = (
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* {monitoring?.enabledFeatures && (
|
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
|
||||||
<Label className="text-muted-foreground">
|
|
||||||
Change Monitoring
|
|
||||||
</Label>
|
|
||||||
<Switch
|
|
||||||
checked={toggleMonitoring}
|
|
||||||
onCheckedChange={setToggleMonitoring}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{toggleMonitoring ? (
|
|
||||||
<ContainerPaidMonitoring
|
|
||||||
appName={data?.appName || ""}
|
|
||||||
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
|
|
||||||
token={
|
|
||||||
monitoring?.metricsConfig?.server?.token || ""
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div> */}
|
|
||||||
<ContainerFreeMonitoring
|
<ContainerFreeMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
/>
|
/>
|
||||||
{/* </div> */}
|
|
||||||
{/* )} */}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -282,11 +256,10 @@ const Postgresql = (
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<ShowDatabaseAdvancedSettings
|
||||||
<ShowCustomCommand id={postgresId} type="postgres" />
|
id={postgresId}
|
||||||
<ShowVolumes id={postgresId} type="postgres" />
|
type="postgres"
|
||||||
<ShowResources id={postgresId} type="postgres" />
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { ShowResources } from "@/components/dashboard/application/advanced/show-resources";
|
|
||||||
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
|
||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
||||||
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
||||||
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
|
|
||||||
import { ShowExternalRedisCredentials } from "@/components/dashboard/redis/general/show-external-redis-credentials";
|
import { ShowExternalRedisCredentials } from "@/components/dashboard/redis/general/show-external-redis-credentials";
|
||||||
import { ShowGeneralRedis } from "@/components/dashboard/redis/general/show-general-redis";
|
import { ShowGeneralRedis } from "@/components/dashboard/redis/general/show-general-redis";
|
||||||
import { ShowInternalRedisCredentials } from "@/components/dashboard/redis/general/show-internal-redis-credentials";
|
import { ShowInternalRedisCredentials } from "@/components/dashboard/redis/general/show-internal-redis-credentials";
|
||||||
import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
|
import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
|
||||||
|
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||||
import { RedisIcon } from "@/components/icons/data-tools-icons";
|
import { RedisIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
@@ -197,10 +195,10 @@ const Redis = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
@@ -272,11 +270,7 @@ const Redis = (
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<ShowDatabaseAdvancedSettings id={redisId} type="redis" />
|
||||||
<ShowCustomCommand id={redisId} type="redis" />
|
|
||||||
<ShowVolumes id={redisId} type="redis" />
|
|
||||||
<ShowResources id={redisId} type="redis" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
28
apps/dokploy/public/templates/datalens.svg
Normal file
28
apps/dokploy/public/templates/datalens.svg
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="163" height="32" fill="none" viewBox="0 0 163 32">
|
||||||
|
<g clip-path="url(#a)">
|
||||||
|
<rect width="32" height="32" fill="#FF6928" rx="16" />
|
||||||
|
<path fill="#fff" fill-rule="evenodd" d="M7.556 29.778C-3 29.778-11.556 23.609-11.556 16c0-7.61 8.557-13.778 19.112-13.778 11.17 0 19.11 5.905 19.11 13.778s-7.94 13.778-19.11 13.778Zm0-8c-10.17 0-17.334-2.587-17.334-5.778 0-3.191 7.163-5.778 17.334-5.778 10.17 0 17.333 2.128 17.333 5.778 0 3.65-7.163 5.778-17.333 5.778Z" clip-rule="evenodd" />
|
||||||
|
<g filter="url(#b)" opacity=".3">
|
||||||
|
<path fill="#FF6928" d="M1.893 11.25C4.192 6.285 5.753 3.667 10.495.177L7.9-5.684-5.308-2.17l-.438 15.1 7.639-1.678Z" />
|
||||||
|
</g>
|
||||||
|
<g filter="url(#c)" opacity=".3">
|
||||||
|
<path fill="#FF6928" d="M12.434 29.6c-4.607-2.955-6.988-4.86-9.798-10.033l-6.16 1.773 1.679 13.563 14.898 2.493-.62-7.796Z" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path fill="currentColor" d="M46.18 22.416c4.02 0 6.03-2.171 6.03-6.514v-.227c0-2.143-.493-3.745-1.48-4.807-.966-1.081-2.502-1.622-4.607-1.622h-1.82v13.17h1.877ZM39.694 5.662h6.656c3.47 0 6.144.892 8.022 2.674 1.763 1.764 2.645 4.19 2.645 7.282v.227c0 3.13-.891 5.585-2.674 7.367C52.485 25.071 49.811 26 46.322 26h-6.628V5.662Zm23.788 20.65c-1.441 0-2.608-.35-3.499-1.052-.986-.777-1.479-1.905-1.479-3.384 0-1.65.72-2.883 2.162-3.698 1.29-.72 3.148-1.081 5.575-1.081h1.678V16.5c0-.949-.18-1.64-.54-2.077-.342-.436-.968-.654-1.878-.654-1.46 0-2.304.701-2.531 2.105h-3.897c.114-1.669.806-2.958 2.076-3.869 1.157-.815 2.693-1.223 4.608-1.223 1.916 0 3.414.417 4.494 1.252 1.157.91 1.736 2.332 1.736 4.266V26h-4.011v-1.792c-1.005 1.403-2.503 2.105-4.494 2.105Zm1.223-2.872c.93 0 1.697-.237 2.304-.711.607-.474.91-1.119.91-1.934v-1.252h-1.593c-1.251 0-2.2.161-2.844.484-.626.322-.939.863-.939 1.621 0 1.195.72 1.792 2.162 1.792Zm15.734 2.844c-3.204 0-4.807-1.564-4.807-4.693v-7.538h-1.905v-2.93h1.905V7.91h4.096v3.215h3.13v2.93h-3.13v7.167c0 1.176.55 1.764 1.65 1.764.607 0 1.129-.095 1.565-.285v3.186c-.759.266-1.593.398-2.504.398Zm8.92.029c-1.442 0-2.608-.35-3.5-1.053-.985-.777-1.478-1.905-1.478-3.384 0-1.65.72-2.883 2.161-3.698 1.29-.72 3.148-1.081 5.576-1.081h1.678V16.5c0-.949-.18-1.64-.54-2.077-.342-.436-.968-.654-1.878-.654-1.46 0-2.304.701-2.532 2.105H84.95c.114-1.669.806-2.958 2.077-3.869 1.157-.815 2.693-1.223 4.608-1.223 1.915 0 3.413.417 4.494 1.252 1.157.91 1.735 2.332 1.735 4.266V26h-4.01v-1.792c-1.005 1.403-2.503 2.105-4.495 2.105Zm1.223-2.873c.929 0 1.697-.237 2.303-.711.607-.474.91-1.119.91-1.934v-1.252h-1.592c-1.252 0-2.2.161-2.845.484-.625.322-.938.863-.938 1.621 0 1.195.72 1.792 2.162 1.792Zm10.671-17.778h4.608v16.726h8.277V26h-12.885V5.662Zm21.632 20.65c-2.313 0-4.162-.672-5.547-2.019-1.479-1.365-2.218-3.214-2.218-5.546v-.228c0-2.313.739-4.19 2.218-5.632 1.442-1.403 3.252-2.105 5.433-2.105 2.067 0 3.755.598 5.063 1.792 1.46 1.328 2.191 3.252 2.191 5.775v1.137h-10.724c.057 1.252.398 2.219 1.024 2.902.645.663 1.536.995 2.674.995 1.782 0 2.816-.692 3.1-2.076h3.897c-.246 1.612-.986 2.854-2.219 3.726-1.213.853-2.844 1.28-4.892 1.28Zm3.129-9.357c-.133-2.219-1.214-3.328-3.243-3.328-.929 0-1.697.294-2.304.881-.588.57-.957 1.385-1.109 2.447h6.656Zm6.19-5.831h4.125v2.36c.929-1.801 2.541-2.702 4.835-2.702 1.498 0 2.703.455 3.613 1.366.929.986 1.394 2.446 1.394 4.38V26h-4.125v-8.875c0-1.024-.209-1.773-.626-2.247-.417-.493-1.081-.74-1.991-.74-.929 0-1.678.285-2.247.854-.569.55-.853 1.356-.853 2.418V26h-4.125V11.124Zm22.385 15.189c-2.01 0-3.575-.427-4.694-1.28-1.118-.853-1.716-2.086-1.792-3.698h3.84c.114.72.361 1.252.74 1.593.379.341 1.005.512 1.877.512 1.555 0 2.333-.54 2.333-1.621 0-.512-.228-.892-.683-1.138-.455-.266-1.233-.474-2.332-.626-2.01-.322-3.414-.806-4.21-1.45-.853-.664-1.28-1.726-1.28-3.186s.607-2.627 1.82-3.499c1.043-.758 2.399-1.138 4.068-1.138 1.725 0 3.119.342 4.181 1.024 1.119.759 1.773 1.953 1.963 3.584h-3.783c-.114-.626-.351-1.08-.711-1.365-.361-.284-.901-.427-1.622-.427-.663 0-1.185.143-1.564.427a1.35 1.35 0 0 0-.541 1.11c0 .473.2.824.598 1.052.417.227 1.175.417 2.275.569 1.953.265 3.385.71 4.295 1.337.986.739 1.48 1.848 1.48 3.328 0 1.592-.55 2.806-1.65 3.64-1.081.835-2.617 1.252-4.608 1.252Z" />
|
||||||
|
<defs>
|
||||||
|
<filter id="b" width="37.575" height="39.945" x="-16.413" y="-16.35" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||||
|
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||||
|
<feGaussianBlur result="effect1_foregroundBlur_219_14417" stdDeviation="5.333" />
|
||||||
|
</filter>
|
||||||
|
<filter id="c" width="37.91" height="39.162" x="-14.19" y="8.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||||
|
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||||
|
<feGaussianBlur result="effect1_foregroundBlur_219_14417" stdDeviation="5.333" />
|
||||||
|
</filter>
|
||||||
|
<clipPath id="a">
|
||||||
|
<rect width="32" height="32" fill="#fff" rx="16" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
7
apps/dokploy/public/templates/hoarder.svg
Normal file
7
apps/dokploy/public/templates/hoarder.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 355 354" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(1,0,0,1,-232,-118)">
|
||||||
|
<path d="M565.33,118.79L253.02,118.79C241.46,118.79 232.09,128.16 232.09,139.72L232.09,450.44C232.09,462 241.46,471.37 253.02,471.37L565.33,471.37C576.89,471.37 586.26,462 586.26,450.44L586.26,139.72C586.26,128.16 576.89,118.79 565.33,118.79ZM386.85,419.57C386.85,422.58 384.4,425.03 381.39,425.03L285.11,425.03C282.1,425.03 279.65,422.58 279.65,419.57L279.65,169.43C279.65,166.42 282.1,163.96 285.11,163.96L379.87,163.96C382.88,163.96 385.33,166.41 385.33,169.43L385.33,264.64C385.33,264.64 384.81,305.61 386.85,337.76L386.85,419.58L386.85,419.57ZM537.19,419.57C537.19,423.92 532.36,426.52 528.75,424.14L484.85,395.44C482.95,394.18 480.5,394.25 478.64,395.59L440.16,423.4C438.56,424.59 436.67,424.66 435.07,424.07C433.66,423.07 432.73,421.43 432.73,419.57L432.73,225.34C437.94,224.34 443.73,223.78 450.43,223.78C483.29,223.78 537.2,242.37 537.2,294.78L537.2,419.58L537.19,419.57Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -25,6 +25,10 @@ import {
|
|||||||
addNewService,
|
addNewService,
|
||||||
checkServiceAccess,
|
checkServiceAccess,
|
||||||
} from "@dokploy/server/services/user";
|
} from "@dokploy/server/services/user";
|
||||||
|
import {
|
||||||
|
getProviderHeaders,
|
||||||
|
type Model,
|
||||||
|
} from "@dokploy/server/utils/ai/select-ai-provider";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -41,6 +45,58 @@ export const aiRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return aiSetting;
|
return aiSetting;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
getModels: protectedProcedure
|
||||||
|
.input(z.object({ apiUrl: z.string().min(1), apiKey: z.string().min(1) }))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
const headers = getProviderHeaders(input.apiUrl, input.apiKey);
|
||||||
|
const response = await fetch(`${input.apiUrl}/models`, { headers });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`Failed to fetch models: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await response.json();
|
||||||
|
|
||||||
|
if (Array.isArray(res)) {
|
||||||
|
return res.map((model) => ({
|
||||||
|
id: model.id || model.name,
|
||||||
|
object: "model",
|
||||||
|
created: Date.now(),
|
||||||
|
owned_by: "provider",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.models) {
|
||||||
|
return res.models.map((model: any) => ({
|
||||||
|
id: model.id || model.name,
|
||||||
|
object: "model",
|
||||||
|
created: Date.now(),
|
||||||
|
owned_by: "provider",
|
||||||
|
})) as Model[];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.data) {
|
||||||
|
return res.data as Model[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const possibleModels =
|
||||||
|
(Object.values(res).find(Array.isArray) as any[]) || [];
|
||||||
|
return possibleModels.map((model) => ({
|
||||||
|
id: model.id || model.name,
|
||||||
|
object: "model",
|
||||||
|
created: Date.now(),
|
||||||
|
owned_by: "provider",
|
||||||
|
})) as Model[];
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: error instanceof Error ? error?.message : `Error: ${error}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
|
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
|
||||||
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -344,6 +344,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
buildPath: input.buildPath,
|
buildPath: input.buildPath,
|
||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
githubId: input.githubId,
|
githubId: input.githubId,
|
||||||
|
watchPaths: input.watchPaths,
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -370,6 +371,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
gitlabId: input.gitlabId,
|
gitlabId: input.gitlabId,
|
||||||
gitlabProjectId: input.gitlabProjectId,
|
gitlabProjectId: input.gitlabProjectId,
|
||||||
gitlabPathNamespace: input.gitlabPathNamespace,
|
gitlabPathNamespace: input.gitlabPathNamespace,
|
||||||
|
watchPaths: input.watchPaths,
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -394,6 +396,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
sourceType: "bitbucket",
|
sourceType: "bitbucket",
|
||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
bitbucketId: input.bitbucketId,
|
bitbucketId: input.bitbucketId,
|
||||||
|
watchPaths: input.watchPaths,
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -440,6 +443,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
customGitSSHKeyId: input.customGitSSHKeyId,
|
customGitSSHKeyId: input.customGitSSHKeyId,
|
||||||
sourceType: "git",
|
sourceType: "git",
|
||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
|
watchPaths: input.watchPaths,
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -668,4 +672,49 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
}),
|
}),
|
||||||
|
move: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
applicationId: z.string(),
|
||||||
|
targetProjectId: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to move this application",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetProject = await findProjectById(input.targetProjectId);
|
||||||
|
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to move to this project",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the application's projectId
|
||||||
|
const updatedApplication = await db
|
||||||
|
.update(applications)
|
||||||
|
.set({
|
||||||
|
projectId: input.targetProjectId,
|
||||||
|
})
|
||||||
|
.where(eq(applications.applicationId, input.applicationId))
|
||||||
|
.returning()
|
||||||
|
.then((res) => res[0]);
|
||||||
|
|
||||||
|
if (!updatedApplication) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to move application",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedApplication;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
apiFindCompose,
|
apiFindCompose,
|
||||||
apiRandomizeCompose,
|
apiRandomizeCompose,
|
||||||
apiUpdateCompose,
|
apiUpdateCompose,
|
||||||
compose,
|
compose as composeTable,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
|
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
|
||||||
import { templates } from "@/templates/templates";
|
import { templates } from "@/templates/templates";
|
||||||
@@ -24,6 +24,7 @@ import { dump } from "js-yaml";
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
@@ -157,8 +158,8 @@ export const composeRouter = createTRPCRouter({
|
|||||||
4;
|
4;
|
||||||
|
|
||||||
const result = await db
|
const result = await db
|
||||||
.delete(compose)
|
.delete(composeTable)
|
||||||
.where(eq(compose.composeId, input.composeId))
|
.where(eq(composeTable.composeId, input.composeId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const cleanupOperations = [
|
const cleanupOperations = [
|
||||||
@@ -501,4 +502,48 @@ export const composeRouter = createTRPCRouter({
|
|||||||
const uniqueTags = _.uniq(allTags);
|
const uniqueTags = _.uniq(allTags);
|
||||||
return uniqueTags;
|
return uniqueTags;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
move: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
composeId: z.string(),
|
||||||
|
targetProjectId: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const compose = await findComposeById(input.composeId);
|
||||||
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to move this compose",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetProject = await findProjectById(input.targetProjectId);
|
||||||
|
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to move to this project",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the compose's projectId
|
||||||
|
const updatedCompose = await db
|
||||||
|
.update(composeTable)
|
||||||
|
.set({
|
||||||
|
projectId: input.targetProjectId,
|
||||||
|
})
|
||||||
|
.where(eq(composeTable.composeId, input.composeId))
|
||||||
|
.returning()
|
||||||
|
.then((res) => res[0]);
|
||||||
|
|
||||||
|
if (!updatedCompose) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to move compose",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedCompose;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
updateDestinationById,
|
updateDestinationById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, desc } from "drizzle-orm";
|
||||||
|
|
||||||
export const destinationRouter = createTRPCRouter({
|
export const destinationRouter = createTRPCRouter({
|
||||||
create: adminProcedure
|
create: adminProcedure
|
||||||
@@ -98,6 +98,7 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
all: protectedProcedure.query(async ({ ctx }) => {
|
all: protectedProcedure.query(async ({ ctx }) => {
|
||||||
return await db.query.destinations.findMany({
|
return await db.query.destinations.findMany({
|
||||||
where: eq(destinations.organizationId, ctx.session.activeOrganizationId),
|
where: eq(destinations.organizationId, ctx.session.activeOrganizationId),
|
||||||
|
orderBy: [desc(destinations.createdAt)],
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
remove: adminProcedure
|
remove: adminProcedure
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
apiSaveEnvironmentVariablesMariaDB,
|
apiSaveEnvironmentVariablesMariaDB,
|
||||||
apiSaveExternalPortMariaDB,
|
apiSaveExternalPortMariaDB,
|
||||||
apiUpdateMariaDB,
|
apiUpdateMariaDB,
|
||||||
|
apiRebuildMariadb,
|
||||||
|
mariadb as mariadbTable,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { cancelJobs } from "@/server/utils/backup";
|
import { cancelJobs } from "@/server/utils/backup";
|
||||||
import {
|
import {
|
||||||
@@ -30,7 +32,10 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import { rebuildDatabase } from "@dokploy/server";
|
||||||
export const mariadbRouter = createTRPCRouter({
|
export const mariadbRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreateMariaDB)
|
.input(apiCreateMariaDB)
|
||||||
@@ -320,6 +325,63 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
move: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
mariadbId: z.string(),
|
||||||
|
targetProjectId: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const mariadb = await findMariadbById(input.mariadbId);
|
||||||
|
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to move this mariadb",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetProject = await findProjectById(input.targetProjectId);
|
||||||
|
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to move to this project",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the mariadb's projectId
|
||||||
|
const updatedMariadb = await db
|
||||||
|
.update(mariadbTable)
|
||||||
|
.set({
|
||||||
|
projectId: input.targetProjectId,
|
||||||
|
})
|
||||||
|
.where(eq(mariadbTable.mariadbId, input.mariadbId))
|
||||||
|
.returning()
|
||||||
|
.then((res) => res[0]);
|
||||||
|
|
||||||
|
if (!updatedMariadb) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to move mariadb",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedMariadb;
|
||||||
|
}),
|
||||||
|
rebuild: protectedProcedure
|
||||||
|
.input(apiRebuildMariadb)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const mariadb = await findMariadbById(input.mariadbId);
|
||||||
|
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to rebuild this MariaDB database",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await rebuildDatabase(mariadb.mariadbId, "mariadb");
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import {
|
|||||||
apiCreateMongo,
|
apiCreateMongo,
|
||||||
apiDeployMongo,
|
apiDeployMongo,
|
||||||
apiFindOneMongo,
|
apiFindOneMongo,
|
||||||
|
apiRebuildMongo,
|
||||||
apiResetMongo,
|
apiResetMongo,
|
||||||
apiSaveEnvironmentVariablesMongo,
|
apiSaveEnvironmentVariablesMongo,
|
||||||
apiSaveExternalPortMongo,
|
apiSaveExternalPortMongo,
|
||||||
apiUpdateMongo,
|
apiUpdateMongo,
|
||||||
|
mongo as mongoTable,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { cancelJobs } from "@/server/utils/backup";
|
import { cancelJobs } from "@/server/utils/backup";
|
||||||
import {
|
import {
|
||||||
@@ -30,7 +32,10 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import { rebuildDatabase } from "@dokploy/server";
|
||||||
export const mongoRouter = createTRPCRouter({
|
export const mongoRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreateMongo)
|
.input(apiCreateMongo)
|
||||||
@@ -334,6 +339,64 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
move: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
mongoId: z.string(),
|
||||||
|
targetProjectId: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const mongo = await findMongoById(input.mongoId);
|
||||||
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to move this mongo",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetProject = await findProjectById(input.targetProjectId);
|
||||||
|
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to move to this project",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the mongo's projectId
|
||||||
|
const updatedMongo = await db
|
||||||
|
.update(mongoTable)
|
||||||
|
.set({
|
||||||
|
projectId: input.targetProjectId,
|
||||||
|
})
|
||||||
|
.where(eq(mongoTable.mongoId, input.mongoId))
|
||||||
|
.returning()
|
||||||
|
.then((res) => res[0]);
|
||||||
|
|
||||||
|
if (!updatedMongo) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to move mongo",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedMongo;
|
||||||
|
}),
|
||||||
|
rebuild: protectedProcedure
|
||||||
|
.input(apiRebuildMongo)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const mongo = await findMongoById(input.mongoId);
|
||||||
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to rebuild this MongoDB database",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await rebuildDatabase(mongo.mongoId, "mongo");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import {
|
|||||||
apiCreateMySql,
|
apiCreateMySql,
|
||||||
apiDeployMySql,
|
apiDeployMySql,
|
||||||
apiFindOneMySql,
|
apiFindOneMySql,
|
||||||
|
apiRebuildMysql,
|
||||||
apiResetMysql,
|
apiResetMysql,
|
||||||
apiSaveEnvironmentVariablesMySql,
|
apiSaveEnvironmentVariablesMySql,
|
||||||
apiSaveExternalPortMySql,
|
apiSaveExternalPortMySql,
|
||||||
apiUpdateMySql,
|
apiUpdateMySql,
|
||||||
|
mysql as mysqlTable,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
@@ -23,6 +25,7 @@ import {
|
|||||||
findBackupsByDbId,
|
findBackupsByDbId,
|
||||||
findMySqlById,
|
findMySqlById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
|
rebuildDatabase,
|
||||||
removeMySqlById,
|
removeMySqlById,
|
||||||
removeService,
|
removeService,
|
||||||
startService,
|
startService,
|
||||||
@@ -32,6 +35,9 @@ import {
|
|||||||
updateMySqlById,
|
updateMySqlById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export const mysqlRouter = createTRPCRouter({
|
export const mysqlRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
@@ -330,6 +336,64 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
move: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
mysqlId: z.string(),
|
||||||
|
targetProjectId: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const mysql = await findMySqlById(input.mysqlId);
|
||||||
|
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to move this mysql",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetProject = await findProjectById(input.targetProjectId);
|
||||||
|
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to move to this project",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the mysql's projectId
|
||||||
|
const updatedMysql = await db
|
||||||
|
.update(mysqlTable)
|
||||||
|
.set({
|
||||||
|
projectId: input.targetProjectId,
|
||||||
|
})
|
||||||
|
.where(eq(mysqlTable.mysqlId, input.mysqlId))
|
||||||
|
.returning()
|
||||||
|
.then((res) => res[0]);
|
||||||
|
|
||||||
|
if (!updatedMysql) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to move mysql",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedMysql;
|
||||||
|
}),
|
||||||
|
rebuild: protectedProcedure
|
||||||
|
.input(apiRebuildMysql)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const mysql = await findMySqlById(input.mysqlId);
|
||||||
|
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to rebuild this MySQL database",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await rebuildDatabase(mysql.mysqlId, "mysql");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -133,6 +133,18 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ownerOrgs = await db.query.organization.findMany({
|
||||||
|
where: eq(organization.ownerId, ctx.user.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ownerOrgs.length <= 1) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message:
|
||||||
|
"You must maintain at least one organization where you are the owner",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const result = await db
|
const result = await db
|
||||||
.delete(organization)
|
.delete(organization)
|
||||||
.where(eq(organization.id, input.organizationId));
|
.where(eq(organization.id, input.organizationId));
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import {
|
|||||||
apiCreatePostgres,
|
apiCreatePostgres,
|
||||||
apiDeployPostgres,
|
apiDeployPostgres,
|
||||||
apiFindOnePostgres,
|
apiFindOnePostgres,
|
||||||
|
apiRebuildPostgres,
|
||||||
apiResetPostgres,
|
apiResetPostgres,
|
||||||
apiSaveEnvironmentVariablesPostgres,
|
apiSaveEnvironmentVariablesPostgres,
|
||||||
apiSaveExternalPortPostgres,
|
apiSaveExternalPortPostgres,
|
||||||
apiUpdatePostgres,
|
apiUpdatePostgres,
|
||||||
|
postgres as postgresTable,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { cancelJobs } from "@/server/utils/backup";
|
import { cancelJobs } from "@/server/utils/backup";
|
||||||
import {
|
import {
|
||||||
@@ -20,6 +22,7 @@ import {
|
|||||||
findBackupsByDbId,
|
findBackupsByDbId,
|
||||||
findPostgresById,
|
findPostgresById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
|
rebuildDatabase,
|
||||||
removePostgresById,
|
removePostgresById,
|
||||||
removeService,
|
removeService,
|
||||||
startService,
|
startService,
|
||||||
@@ -30,7 +33,9 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "@/server/db";
|
||||||
export const postgresRouter = createTRPCRouter({
|
export const postgresRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreatePostgres)
|
.input(apiCreatePostgres)
|
||||||
@@ -350,6 +355,68 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
move: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
postgresId: z.string(),
|
||||||
|
targetProjectId: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
|
if (
|
||||||
|
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to move this postgres",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetProject = await findProjectById(input.targetProjectId);
|
||||||
|
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to move to this project",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the postgres's projectId
|
||||||
|
const updatedPostgres = await db
|
||||||
|
.update(postgresTable)
|
||||||
|
.set({
|
||||||
|
projectId: input.targetProjectId,
|
||||||
|
})
|
||||||
|
.where(eq(postgresTable.postgresId, input.postgresId))
|
||||||
|
.returning()
|
||||||
|
.then((res) => res[0]);
|
||||||
|
|
||||||
|
if (!updatedPostgres) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to move postgres",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedPostgres;
|
||||||
|
}),
|
||||||
|
rebuild: protectedProcedure
|
||||||
|
.input(apiRebuildPostgres)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
|
if (
|
||||||
|
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to rebuild this Postgres database",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await rebuildDatabase(postgres.postgresId, "postgres");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
apiSaveEnvironmentVariablesRedis,
|
apiSaveEnvironmentVariablesRedis,
|
||||||
apiSaveExternalPortRedis,
|
apiSaveExternalPortRedis,
|
||||||
apiUpdateRedis,
|
apiUpdateRedis,
|
||||||
|
redis as redisTable,
|
||||||
|
apiRebuildRedis,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
@@ -30,7 +32,10 @@ import {
|
|||||||
updateRedisById,
|
updateRedisById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { rebuildDatabase } from "@dokploy/server";
|
||||||
export const redisRouter = createTRPCRouter({
|
export const redisRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreateRedis)
|
.input(apiCreateRedis)
|
||||||
@@ -314,6 +319,63 @@ export const redisRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
move: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
redisId: z.string(),
|
||||||
|
targetProjectId: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const redis = await findRedisById(input.redisId);
|
||||||
|
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to move this redis",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetProject = await findProjectById(input.targetProjectId);
|
||||||
|
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to move to this project",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the redis's projectId
|
||||||
|
const updatedRedis = await db
|
||||||
|
.update(redisTable)
|
||||||
|
.set({
|
||||||
|
projectId: input.targetProjectId,
|
||||||
|
})
|
||||||
|
.where(eq(redisTable.redisId, input.redisId))
|
||||||
|
.returning()
|
||||||
|
.then((res) => res[0]);
|
||||||
|
|
||||||
|
if (!updatedRedis) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to move redis",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedRedis;
|
||||||
|
}),
|
||||||
|
rebuild: protectedProcedure
|
||||||
|
.input(apiRebuildRedis)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const redis = await findRedisById(input.redisId);
|
||||||
|
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to rebuild this Redis database",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await rebuildDatabase(redis.redisId, "redis");
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -206,6 +206,10 @@ export const serverRouter = createTRPCRouter({
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
|
railpack: {
|
||||||
|
enabled: boolean;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
isDokployNetworkInstalled: boolean;
|
isDokployNetworkInstalled: boolean;
|
||||||
isSwarmInstalled: boolean;
|
isSwarmInstalled: boolean;
|
||||||
isMainDirectoryInstalled: boolean;
|
isMainDirectoryInstalled: boolean;
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
getDokployImageTag,
|
getDokployImageTag,
|
||||||
getUpdateData,
|
getUpdateData,
|
||||||
initializeTraefik,
|
initializeTraefik,
|
||||||
logRotationManager,
|
|
||||||
parseRawConfig,
|
parseRawConfig,
|
||||||
paths,
|
paths,
|
||||||
prepareEnvironmentVariables,
|
prepareEnvironmentVariables,
|
||||||
@@ -49,6 +48,9 @@ import {
|
|||||||
writeConfig,
|
writeConfig,
|
||||||
writeMainConfig,
|
writeMainConfig,
|
||||||
writeTraefikConfigInPath,
|
writeTraefikConfigInPath,
|
||||||
|
startLogCleanup,
|
||||||
|
stopLogCleanup,
|
||||||
|
getLogCleanupStatus,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
|
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
|
||||||
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||||
@@ -570,48 +572,51 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const rawConfig = readMonitoringConfig();
|
const rawConfig = readMonitoringConfig(
|
||||||
|
!!input.dateRange?.start && !!input.dateRange?.end,
|
||||||
|
);
|
||||||
|
|
||||||
const parsedConfig = parseRawConfig(
|
const parsedConfig = parseRawConfig(
|
||||||
rawConfig as string,
|
rawConfig as string,
|
||||||
input.page,
|
input.page,
|
||||||
input.sort,
|
input.sort,
|
||||||
input.search,
|
input.search,
|
||||||
input.status,
|
input.status,
|
||||||
|
input.dateRange,
|
||||||
);
|
);
|
||||||
|
|
||||||
return parsedConfig;
|
return parsedConfig;
|
||||||
}),
|
}),
|
||||||
readStats: adminProcedure.query(() => {
|
readStats: adminProcedure
|
||||||
if (IS_CLOUD) {
|
.meta({
|
||||||
return [];
|
openapi: {
|
||||||
}
|
path: "/read-stats",
|
||||||
const rawConfig = readMonitoringConfig();
|
method: "POST",
|
||||||
const processedLogs = processLogs(rawConfig as string);
|
override: true,
|
||||||
return processedLogs || [];
|
enabled: false,
|
||||||
}),
|
},
|
||||||
getLogRotateStatus: adminProcedure.query(async () => {
|
})
|
||||||
if (IS_CLOUD) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return await logRotationManager.getStatus();
|
|
||||||
}),
|
|
||||||
toggleLogRotate: adminProcedure
|
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z
|
||||||
enable: z.boolean(),
|
.object({
|
||||||
}),
|
dateRange: z
|
||||||
|
.object({
|
||||||
|
start: z.string().optional(),
|
||||||
|
end: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.query(({ input }) => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return [];
|
||||||
}
|
}
|
||||||
if (input.enable) {
|
const rawConfig = readMonitoringConfig(
|
||||||
await logRotationManager.activate();
|
!!input?.dateRange?.start || !!input?.dateRange?.end,
|
||||||
} else {
|
);
|
||||||
await logRotationManager.deactivate();
|
const processedLogs = processLogs(rawConfig as string, input?.dateRange);
|
||||||
}
|
return processedLogs || [];
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
}),
|
||||||
haveActivateRequests: adminProcedure.query(async () => {
|
haveActivateRequests: adminProcedure.query(async () => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
@@ -831,10 +836,20 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
updateLogCleanup: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
cronExpression: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
if (input.cronExpression) {
|
||||||
|
return startLogCleanup(input.cronExpression);
|
||||||
|
}
|
||||||
|
return stopLogCleanup();
|
||||||
|
}),
|
||||||
|
|
||||||
|
getLogCleanupStatus: adminProcedure.query(async () => {
|
||||||
|
return getLogCleanupStatus();
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
// {
|
|
||||||
// "Parallelism": 1,
|
|
||||||
// "Delay": 10000000000,
|
|
||||||
// "FailureAction": "rollback",
|
|
||||||
// "Order": "start-first"
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ export const domain = z
|
|||||||
.max(65535, { message: "Port must be 65535 or below" })
|
.max(65535, { message: "Port must be 65535 or below" })
|
||||||
.optional(),
|
.optional(),
|
||||||
https: z.boolean().optional(),
|
https: z.boolean().optional(),
|
||||||
certificateType: z.enum(["letsencrypt", "none"]).optional(),
|
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||||
|
customCertResolver: z.string().optional(),
|
||||||
})
|
})
|
||||||
.superRefine((input, ctx) => {
|
.superRefine((input, ctx) => {
|
||||||
if (input.https && !input.certificateType) {
|
if (input.https && !input.certificateType) {
|
||||||
@@ -20,6 +21,14 @@ export const domain = z
|
|||||||
message: "Required",
|
message: "Required",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.certificateType === "custom" && !input.customCertResolver) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["customCertResolver"],
|
||||||
|
message: "Required",
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const domainCompose = z
|
export const domainCompose = z
|
||||||
@@ -32,7 +41,8 @@ export const domainCompose = z
|
|||||||
.max(65535, { message: "Port must be 65535 or below" })
|
.max(65535, { message: "Port must be 65535 or below" })
|
||||||
.optional(),
|
.optional(),
|
||||||
https: z.boolean().optional(),
|
https: z.boolean().optional(),
|
||||||
certificateType: z.enum(["letsencrypt", "none"]).optional(),
|
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||||
|
customCertResolver: z.string().optional(),
|
||||||
serviceName: z.string().min(1, { message: "Service name is required" }),
|
serviceName: z.string().min(1, { message: "Service name is required" }),
|
||||||
})
|
})
|
||||||
.superRefine((input, ctx) => {
|
.superRefine((input, ctx) => {
|
||||||
@@ -43,4 +53,12 @@ export const domainCompose = z
|
|||||||
message: "Required",
|
message: "Required",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.certificateType === "custom" && !input.customCertResolver) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["customCertResolver"],
|
||||||
|
message: "Required",
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
96
apps/dokploy/templates/datalens/docker-compose.yml
Normal file
96
apps/dokploy/templates/datalens/docker-compose.yml
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
services:
|
||||||
|
pg-compeng:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: "postgres"
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
|
||||||
|
control-api:
|
||||||
|
image: ghcr.io/datalens-tech/datalens-control-api:0.2192.0
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
BI_API_UWSGI_WORKERS_COUNT: 4
|
||||||
|
CONNECTOR_AVAILABILITY_VISIBLE: "clickhouse,postgres,chyt,ydb,mysql,greenplum,mssql,appmetrica_api,metrika_api"
|
||||||
|
RQE_FORCE_OFF: 1
|
||||||
|
DL_CRY_ACTUAL_KEY_ID: key_1
|
||||||
|
DL_CRY_KEY_VAL_ID_key_1: "h1ZpilcYLYRdWp7Nk8X1M1kBPiUi8rdjz9oBfHyUKIk="
|
||||||
|
RQE_SECRET_KEY: ""
|
||||||
|
US_HOST: "http://us:8083"
|
||||||
|
US_MASTER_TOKEN: "fake-us-master-token"
|
||||||
|
depends_on:
|
||||||
|
- us
|
||||||
|
|
||||||
|
data-api:
|
||||||
|
container_name: datalens-data-api
|
||||||
|
image: ghcr.io/datalens-tech/datalens-data-api:0.2192.0
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
GUNICORN_WORKERS_COUNT: 5
|
||||||
|
RQE_FORCE_OFF: 1
|
||||||
|
CACHES_ON: 0
|
||||||
|
MUTATIONS_CACHES_ON: 0
|
||||||
|
RQE_SECRET_KEY: ""
|
||||||
|
DL_CRY_ACTUAL_KEY_ID: key_1
|
||||||
|
DL_CRY_KEY_VAL_ID_key_1: "h1ZpilcYLYRdWp7Nk8X1M1kBPiUi8rdjz9oBfHyUKIk="
|
||||||
|
BI_COMPENG_PG_ON: 1
|
||||||
|
BI_COMPENG_PG_URL: "postgresql://postgres:postgres@pg-compeng:5432/postgres"
|
||||||
|
US_HOST: "http://us:8083"
|
||||||
|
US_MASTER_TOKEN: "fake-us-master-token"
|
||||||
|
depends_on:
|
||||||
|
- us
|
||||||
|
- pg-compeng
|
||||||
|
|
||||||
|
pg-us:
|
||||||
|
container_name: datalens-pg-us
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: us-db-ci_purgeable
|
||||||
|
POSTGRES_USER: us
|
||||||
|
POSTGRES_PASSWORD: us
|
||||||
|
volumes:
|
||||||
|
- ${VOLUME_US:-./metadata}:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
us:
|
||||||
|
image: ghcr.io/datalens-tech/datalens-us:0.310.0
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- pg-us
|
||||||
|
environment:
|
||||||
|
APP_INSTALLATION: "opensource"
|
||||||
|
APP_ENV: "prod"
|
||||||
|
MASTER_TOKEN: "fake-us-master-token"
|
||||||
|
POSTGRES_DSN_LIST: ${METADATA_POSTGRES_DSN_LIST:-postgres://us:us@pg-us:5432/us-db-ci_purgeable}
|
||||||
|
SKIP_INSTALL_DB_EXTENSIONS: ${METADATA_SKIP_INSTALL_DB_EXTENSIONS:-0}
|
||||||
|
USE_DEMO_DATA: ${USE_DEMO_DATA:-0}
|
||||||
|
HC: ${HC:-0}
|
||||||
|
NODE_EXTRA_CA_CERTS: /certs/root.crt
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
volumes:
|
||||||
|
- ./certs:/certs
|
||||||
|
|
||||||
|
datalens:
|
||||||
|
image: ghcr.io/datalens-tech/datalens-ui:0.2601.0
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- ${UI_PORT:-8080}:8080
|
||||||
|
depends_on:
|
||||||
|
- us
|
||||||
|
- control-api
|
||||||
|
- data-api
|
||||||
|
environment:
|
||||||
|
APP_MODE: "full"
|
||||||
|
APP_ENV: "production"
|
||||||
|
APP_INSTALLATION: "opensource"
|
||||||
|
AUTH_POLICY: "disabled"
|
||||||
|
US_ENDPOINT: "http://us:8083"
|
||||||
|
BI_API_ENDPOINT: "http://control-api:8080"
|
||||||
|
BI_DATA_ENDPOINT: "http://data-api:8080"
|
||||||
|
US_MASTER_TOKEN: "fake-us-master-token"
|
||||||
|
NODE_EXTRA_CA_CERTS: "/usr/local/share/ca-certificates/cert.pem"
|
||||||
|
HC: ${HC:-0}
|
||||||
|
YANDEX_MAP_ENABLED: ${YANDEX_MAP_ENABLED:-0}
|
||||||
|
YANDEX_MAP_TOKEN: ${YANDEX_MAP_TOKEN:-0}
|
||||||
23
apps/dokploy/templates/datalens/index.ts
Normal file
23
apps/dokploy/templates/datalens/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: generateRandomDomain(schema),
|
||||||
|
port: 8080,
|
||||||
|
serviceName: "datalens",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const envs = ["HC=1"];
|
||||||
|
|
||||||
|
return {
|
||||||
|
envs,
|
||||||
|
domains,
|
||||||
|
};
|
||||||
|
}
|
||||||
45
apps/dokploy/templates/hoarder/docker-compose.yml
Normal file
45
apps/dokploy/templates/hoarder/docker-compose.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: ghcr.io/hoarder-app/hoarder:0.22.0
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- hoarder-data:/data
|
||||||
|
ports:
|
||||||
|
- 3000
|
||||||
|
environment:
|
||||||
|
- DISABLE_SIGNUPS
|
||||||
|
- NEXTAUTH_URL
|
||||||
|
- NEXTAUTH_SECRET
|
||||||
|
- MEILI_ADDR=http://meilisearch:7700
|
||||||
|
- BROWSER_WEB_URL=http://chrome:9222
|
||||||
|
- DATA_DIR=/data
|
||||||
|
chrome:
|
||||||
|
image: gcr.io/zenika-hub/alpine-chrome:124
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- --no-sandbox
|
||||||
|
- --disable-gpu
|
||||||
|
- --disable-dev-shm-usage
|
||||||
|
- --remote-debugging-address=0.0.0.0
|
||||||
|
- --remote-debugging-port=9222
|
||||||
|
- --hide-scrollbars
|
||||||
|
meilisearch:
|
||||||
|
image: getmeili/meilisearch:v1.6
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- MEILI_MASTER_KEY
|
||||||
|
- MEILI_NO_ANALYTICS="true"
|
||||||
|
volumes:
|
||||||
|
- meilisearch-data:/meili_data
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- curl
|
||||||
|
- '-f'
|
||||||
|
- 'http://127.0.0.1:7700/health'
|
||||||
|
interval: 2s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 15
|
||||||
|
volumes:
|
||||||
|
meilisearch-data:
|
||||||
|
hoarder-data:
|
||||||
34
apps/dokploy/templates/hoarder/index.ts
Normal file
34
apps/dokploy/templates/hoarder/index.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generateBase64,
|
||||||
|
generatePassword,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const mainDomain = generateRandomDomain(schema);
|
||||||
|
const postgresPassword = generatePassword();
|
||||||
|
const nextSecret = generateBase64(32);
|
||||||
|
const meiliMasterKey = generateBase64(32);
|
||||||
|
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: mainDomain,
|
||||||
|
port: 3000,
|
||||||
|
serviceName: "web",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
`NEXTAUTH_SECRET=${nextSecret}`,
|
||||||
|
`MEILI_MASTER_KEY=${meiliMasterKey}`,
|
||||||
|
`NEXTAUTH_URL=http://${mainDomain}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
domains,
|
||||||
|
envs,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# This is an UNOFFICIAL production docker image build for Superset:
|
# This is an UNOFFICIAL production docker image build for Superset:
|
||||||
# - https://github.com/amancevice/docker-superset
|
# - https://github.com/amancevice/docker-superset
|
||||||
|
|
||||||
|
|
||||||
# ## SETUP INSTRUCTIONS
|
# ## SETUP INSTRUCTIONS
|
||||||
#
|
#
|
||||||
# After deploying this image, you will need to run one of the two
|
# After deploying this image, you will need to run one of the two
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
# $ superset-init # Initialise database only
|
# $ superset-init # Initialise database only
|
||||||
#
|
#
|
||||||
# You will be prompted to enter the credentials for the admin user.
|
# You will be prompted to enter the credentials for the admin user.
|
||||||
|
|
||||||
|
|
||||||
# ## NETWORK INSTRUCTIONS
|
# ## NETWORK INSTRUCTIONS
|
||||||
#
|
#
|
||||||
@@ -66,7 +66,7 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
superset_redis:
|
superset_redis:
|
||||||
image: redis
|
image: redis
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user