mirror of
https://github.com/vitodeploy/vito.git
synced 2025-07-03 06:56:15 +00:00
#591 - sites
This commit is contained in:
@ -0,0 +1,77 @@
|
||||
import { Head, usePage } from '@inertiajs/react';
|
||||
import { Site } from '@/types/site';
|
||||
import ServerLayout from '@/layouts/server/layout';
|
||||
import { Server } from '@/types/server';
|
||||
import Container from '@/components/container';
|
||||
import HeaderContainer from '@/components/header-container';
|
||||
import Heading from '@/components/heading';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { BookOpenIcon, MoreHorizontalIcon, RocketIcon } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { PaginatedData } from '@/types';
|
||||
import { Deployment } from '@/types/deployment';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||
import DeploymentScript from '@/pages/application/components/deployment-script';
|
||||
import Env from '@/pages/application/components/env';
|
||||
import Deploy from '@/pages/application/components/deploy';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { columns } from '@/pages/application/components/deployment-columns';
|
||||
import AutoDeployment from '@/pages/application/components/auto-deployment';
|
||||
|
||||
export default function AppWithDeployment() {
|
||||
const page = usePage<{
|
||||
server: Server;
|
||||
site: Site;
|
||||
deployments: PaginatedData<Deployment>;
|
||||
deploymentScript: string;
|
||||
}>();
|
||||
|
||||
return (
|
||||
<ServerLayout>
|
||||
<Head title={`${page.props.site.domain} - ${page.props.server.name}`} />
|
||||
|
||||
<Container className="max-w-5xl">
|
||||
<HeaderContainer>
|
||||
<Heading title="Application" description="Here you can manage the deployed application" />
|
||||
<div className="flex items-center gap-2">
|
||||
<a href="https://vitodeploy.com/docs/sites/application" target="_blank">
|
||||
<Button variant="outline">
|
||||
<BookOpenIcon />
|
||||
<span className="hidden lg:block">Docs</span>
|
||||
</Button>
|
||||
</a>
|
||||
<Deploy site={page.props.site}>
|
||||
<Button>
|
||||
<RocketIcon />
|
||||
<span className="hidden lg:block">Deploy</span>
|
||||
</Button>
|
||||
</Deploy>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontalIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<AutoDeployment site={page.props.site}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()} disabled={!page.props.site.source_control_id}>
|
||||
{page.props.site.auto_deploy ? 'Disable' : 'Enable'} auto deploy
|
||||
</DropdownMenuItem>
|
||||
</AutoDeployment>
|
||||
<DeploymentScript site={page.props.site} script={page.props.deploymentScript}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>Deployment Script</DropdownMenuItem>
|
||||
</DeploymentScript>
|
||||
<Env site={page.props.site}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>Update .env</DropdownMenuItem>
|
||||
</Env>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</HeaderContainer>
|
||||
|
||||
<DataTable columns={columns} paginatedData={page.props.deployments} />
|
||||
</Container>
|
||||
</ServerLayout>
|
||||
);
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { useForm } from '@inertiajs/react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LoaderCircleIcon } from 'lucide-react';
|
||||
import FormSuccessful from '@/components/form-successful';
|
||||
import { Site } from '@/types/site';
|
||||
|
||||
export default function AutoDeployment({ site, children }: { site: Site; children: ReactNode }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const form = useForm();
|
||||
|
||||
const submit = () => {
|
||||
const url = site.auto_deploy
|
||||
? route('application.disable-auto-deployment', {
|
||||
server: site.server_id,
|
||||
site: site.id,
|
||||
})
|
||||
: route('application.enable-auto-deployment', { server: site.server_id, site: site.id });
|
||||
form.post(url, {
|
||||
onSuccess: () => {
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{site.auto_deploy ? 'Disable' : 'Enable'} auto deployment</DialogTitle>
|
||||
<DialogDescription className="sr-only">{site.auto_deploy ? 'Disable' : 'Enable'} auto deployment</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2 p-4">
|
||||
<p>Are you sure you want to {site.auto_deploy ? 'disable' : 'enable'} auto deployment?</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button disabled={form.processing} onClick={submit}>
|
||||
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
||||
<FormSuccessful successful={form.recentlySuccessful} />
|
||||
Yes
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
53
resources/js/pages/application/components/deploy.tsx
Normal file
53
resources/js/pages/application/components/deploy.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { useForm } from '@inertiajs/react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LoaderCircleIcon } from 'lucide-react';
|
||||
import FormSuccessful from '@/components/form-successful';
|
||||
import { Site } from '@/types/site';
|
||||
|
||||
export default function Deploy({ site, children }: { site: Site; children: ReactNode }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const form = useForm();
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('application.deploy', { server: site.server_id, site: site.id }), {
|
||||
onSuccess: () => {
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Deploy</DialogTitle>
|
||||
<DialogDescription className="sr-only">Deploy application</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2 p-4">
|
||||
<p>Are you sure you want to deploy this site?</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button disabled={form.processing} onClick={submit}>
|
||||
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
||||
<FormSuccessful successful={form.recentlySuccessful} />
|
||||
Deploy
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { MoreVerticalIcon } from 'lucide-react';
|
||||
import DateTime from '@/components/date-time';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||
import { Deployment } from '@/types/deployment';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Download, View } from '@/pages/server-logs/components/columns';
|
||||
|
||||
export const columns: ColumnDef<Deployment>[] = [
|
||||
{
|
||||
accessorKey: 'commit_id',
|
||||
header: 'Commit',
|
||||
enableColumnFilter: true,
|
||||
cell: ({ row }) => {
|
||||
return row.original.commit_data?.message ?? 'No commit message';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: 'Deployed At',
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
return <DateTime date={row.original.created_at} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
enableColumnFilter: true,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
return <Badge variant={row.original.status_color}>{row.original.status}</Badge>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
enableColumnFilter: false,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreVerticalIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<View serverLog={row.original.log} />
|
||||
<Download serverLog={row.original.log}>
|
||||
<DropdownMenuItem>Download</DropdownMenuItem>
|
||||
</Download>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
@ -0,0 +1,69 @@
|
||||
import React, { FormEvent, ReactNode, useState } from 'react';
|
||||
import { useForm } from '@inertiajs/react';
|
||||
import { Editor, useMonaco } from '@monaco-editor/react';
|
||||
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LoaderCircleIcon } from 'lucide-react';
|
||||
import { Site } from '@/types/site';
|
||||
import { registerBashLanguage } from '@/lib/editor';
|
||||
import InputError from '@/components/ui/input-error';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
|
||||
export default function DeploymentScript({ site, script, children }: { site: Site; script: string; children: ReactNode }) {
|
||||
const { getActualAppearance } = useAppearance();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const form = useForm<{
|
||||
script: string;
|
||||
}>({
|
||||
script: script,
|
||||
});
|
||||
|
||||
const submit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
form.put(route('application.update-deployment-script', { server: site.server_id, site: site.id }), {
|
||||
onSuccess: () => {
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
registerBashLanguage(useMonaco());
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>{children}</SheetTrigger>
|
||||
<SheetContent className="sm:max-w-5xl">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Deployment script</SheetTitle>
|
||||
<SheetDescription className="sr-only">Update deployment script</SheetDescription>
|
||||
</SheetHeader>
|
||||
<Form id="update-script-form" className="h-full" onSubmit={submit}>
|
||||
<Editor
|
||||
defaultLanguage="bash"
|
||||
defaultValue={form.data.script}
|
||||
theme={getActualAppearance() === 'dark' ? 'vs-dark' : 'vs'}
|
||||
className="h-full"
|
||||
onChange={(value) => form.setData('script', value ?? '')}
|
||||
options={{
|
||||
fontSize: 15,
|
||||
}}
|
||||
/>
|
||||
</Form>
|
||||
<SheetFooter>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button form="update-script-form" disabled={form.processing} onClick={submit} className="ml-2">
|
||||
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
||||
Save
|
||||
</Button>
|
||||
<SheetClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</SheetClose>
|
||||
<InputError message={form.errors.script} />
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
91
resources/js/pages/application/components/env.tsx
Normal file
91
resources/js/pages/application/components/env.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import React, { FormEvent, ReactNode, useState } from 'react';
|
||||
import { useForm } from '@inertiajs/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { Editor, useMonaco } from '@monaco-editor/react';
|
||||
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LoaderCircleIcon } from 'lucide-react';
|
||||
import { registerDotEnvLanguage } from '@/lib/editor';
|
||||
import { Site } from '@/types/site';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
|
||||
export default function Env({ site, children }: { site: Site; children: ReactNode }) {
|
||||
const { getActualAppearance } = useAppearance();
|
||||
const [open, setOpen] = useState(false);
|
||||
const form = useForm<{
|
||||
env: string;
|
||||
}>({
|
||||
env: '',
|
||||
});
|
||||
|
||||
const submit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
form.put(route('application.update-env', { server: site.server_id, site: site.id }), {
|
||||
onSuccess: () => {
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['application.env', site.server_id, site.id],
|
||||
queryFn: async () => {
|
||||
const response = await axios.get(
|
||||
route('application.env', {
|
||||
server: site.server_id,
|
||||
site: site.id,
|
||||
}),
|
||||
);
|
||||
if (response.data?.env) {
|
||||
form.setData('env', response.data.env);
|
||||
}
|
||||
return response.data;
|
||||
},
|
||||
retry: false,
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
registerDotEnvLanguage(useMonaco());
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>{children}</SheetTrigger>
|
||||
<SheetContent className="sm:max-w-5xl">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Edit .env</SheetTitle>
|
||||
<SheetDescription className="sr-only">Edit .env file</SheetDescription>
|
||||
</SheetHeader>
|
||||
<Form id="update-env-form" className="h-full" onSubmit={submit}>
|
||||
{query.isSuccess ? (
|
||||
<Editor
|
||||
defaultLanguage="dotenv"
|
||||
defaultValue={query.data.env}
|
||||
theme={getActualAppearance() === 'dark' ? 'vs-dark' : 'vs'}
|
||||
className="h-full"
|
||||
onChange={(value) => form.setData('env', value ?? '')}
|
||||
options={{
|
||||
fontSize: 15,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton className="h-full w-full rounded-none" />
|
||||
)}
|
||||
</Form>
|
||||
<SheetFooter>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button form="update-env-form" disabled={form.processing || query.isLoading} onClick={submit} className="ml-2">
|
||||
{(form.processing || query.isLoading) && <LoaderCircleIcon className="animate-spin" />}
|
||||
Save
|
||||
</Button>
|
||||
<SheetClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</SheetClose>
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
108
resources/js/pages/application/components/load-balancer.tsx
Normal file
108
resources/js/pages/application/components/load-balancer.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import { Head, useForm, usePage } from '@inertiajs/react';
|
||||
import { Site } from '@/types/site';
|
||||
import ServerLayout from '@/layouts/server/layout';
|
||||
import { Server } from '@/types/server';
|
||||
import Container from '@/components/container';
|
||||
import HeaderContainer from '@/components/header-container';
|
||||
import Heading from '@/components/heading';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { BookOpenIcon, LoaderCircleIcon } from 'lucide-react';
|
||||
import React, { FormEvent } from 'react';
|
||||
import { LoadBalancerServer } from '@/types/load-balancer-server';
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Form, FormField, FormFields } from '@/components/ui/form';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import type { Project } from '@/types/project';
|
||||
import InputError from '@/components/ui/input-error';
|
||||
import FormSuccessful from '@/components/form-successful';
|
||||
|
||||
export default function LoadBalancer() {
|
||||
const page = usePage<{
|
||||
server: Server;
|
||||
site: Site;
|
||||
loadBalancerServers: LoadBalancerServer[];
|
||||
}>();
|
||||
|
||||
const form = useForm<{
|
||||
method: 'round-robin' | 'least-connections' | 'ip-hash';
|
||||
servers: {
|
||||
server: string;
|
||||
port: string;
|
||||
weight: string;
|
||||
backup: boolean;
|
||||
}[];
|
||||
}>({
|
||||
method: 'round-robin',
|
||||
servers: [],
|
||||
});
|
||||
|
||||
const submit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
form.post(route('application.update-load-balancer', { server: page.props.server.id, site: page.props.site.id }), {
|
||||
onSuccess: () => {
|
||||
form.reset();
|
||||
},
|
||||
preserveScroll: true,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ServerLayout>
|
||||
<Head title={`${page.props.site.domain} - ${page.props.server.name}`} />
|
||||
|
||||
<Container className="max-w-5xl">
|
||||
<HeaderContainer>
|
||||
<Heading title="Load balancer" description="Here you can manage the load balancer configs" />
|
||||
<div className="flex items-center gap-2">
|
||||
<a href="https://vitodeploy.com/docs/sites/load-balancer" target="_blank">
|
||||
<Button variant="outline">
|
||||
<BookOpenIcon />
|
||||
<span className="hidden lg:block">Docs</span>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</HeaderContainer>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Configs</CardTitle>
|
||||
<CardDescription>Modify load balancer configs</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4">
|
||||
<Form>
|
||||
<FormFields>
|
||||
<FormField>
|
||||
<Label htmlFor="method">Method</Label>
|
||||
<Select
|
||||
value={form.data.method}
|
||||
onValueChange={(value) => form.setData('method', value as 'round-robin' | 'least-connections' | 'ip-hash')}
|
||||
>
|
||||
<SelectTrigger id="method">
|
||||
<SelectValue placeholder="Select a method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="round-robin">round-robin</SelectItem>
|
||||
<SelectItem value="least-connections">least-connections</SelectItem>
|
||||
<SelectItem value="ip-hash">ip-hash</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={form.errors.method} />
|
||||
</FormField>
|
||||
</FormFields>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button disabled={form.processing} onClick={submit}>
|
||||
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
||||
<FormSuccessful successful={form.recentlySuccessful} />
|
||||
Save and Deploy
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Container>
|
||||
</ServerLayout>
|
||||
);
|
||||
}
|
20
resources/js/pages/application/index.tsx
Normal file
20
resources/js/pages/application/index.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { usePage } from '@inertiajs/react';
|
||||
import { Site } from '@/types/site';
|
||||
import React from 'react';
|
||||
import AppWithDeployment from '@/pages/application/components/app-with-deployment';
|
||||
import LoadBalancer from '@/pages/application/components/load-balancer';
|
||||
import siteHelper from '@/lib/site-helper';
|
||||
|
||||
export default function Application() {
|
||||
const page = usePage<{
|
||||
site: Site;
|
||||
}>();
|
||||
|
||||
siteHelper.storeSite(page.props.site);
|
||||
|
||||
if (page.props.site.type === 'load-balancer') {
|
||||
return <LoadBalancer />;
|
||||
}
|
||||
|
||||
return <AppWithDeployment />;
|
||||
}
|
Reference in New Issue
Block a user