mirror of
https://github.com/vitodeploy/vito.git
synced 2025-07-03 15:02:34 +00:00
#591 - sites [wip]
This commit is contained in:
89
resources/js/pages/sites/components/columns.tsx
Normal file
89
resources/js/pages/sites/components/columns.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Server } from '@/types/server';
|
||||
import { Link } from '@inertiajs/react';
|
||||
import DateTime from '@/components/date-time';
|
||||
import { Site } from '@/types/site';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { EyeIcon } from 'lucide-react';
|
||||
|
||||
export default function getColumns(server?: Server): ColumnDef<Site>[] {
|
||||
let columns: ColumnDef<Site>[] = [
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
enableColumnFilter: true,
|
||||
enableSorting: true,
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'domain',
|
||||
header: 'Domain',
|
||||
enableColumnFilter: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'type',
|
||||
header: 'Type',
|
||||
enableColumnFilter: true,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
return <Badge variant="outline">{row.original.type}</Badge>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: 'Created at',
|
||||
enableColumnFilter: true,
|
||||
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">
|
||||
<Link href={route('sites.show', { server: row.original.server_id, site: row.original.id })} prefetch>
|
||||
<Button variant="outline" size="sm">
|
||||
<EyeIcon />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (!server) {
|
||||
// add column to the first
|
||||
columns = [
|
||||
{
|
||||
id: 'server',
|
||||
header: 'Server',
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Link href={route('servers.show', { server: row.original.server_id })} prefetch>
|
||||
{row.original.server?.name}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
...columns,
|
||||
];
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
189
resources/js/pages/sites/components/create-site.tsx
Normal file
189
resources/js/pages/sites/components/create-site.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import { ReactNode, useState, FormEventHandler } from 'react';
|
||||
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { Form, FormField, FormFields } from '@/components/ui/form';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
import { useForm, usePage } from '@inertiajs/react';
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import InputError from '@/components/ui/input-error';
|
||||
import type { SharedData } from '@/types';
|
||||
import SourceControlSelect from '@/pages/source-controls/components/source-control-select';
|
||||
import { Server } from '@/types/server';
|
||||
import ServerSelect from '@/pages/servers/components/server-select';
|
||||
import ServiceVersionSelect from '@/pages/services/components/service-version-select';
|
||||
import { DynamicFieldConfig } from '@/types/dynamic-field-config';
|
||||
import DynamicField from '@/components/ui/dynamic-field';
|
||||
import { TagsInput } from '@/components/ui/tags-input';
|
||||
|
||||
type CreateSiteForm = {
|
||||
server: string;
|
||||
type: string;
|
||||
domain: string;
|
||||
aliases: string[];
|
||||
php_version: string;
|
||||
source_control: string;
|
||||
user: string;
|
||||
};
|
||||
|
||||
export default function CreateSite({ server, children }: { server?: Server; children: ReactNode }) {
|
||||
const page = usePage<SharedData>();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const form = useForm<CreateSiteForm>({
|
||||
server: server?.id.toString() || '',
|
||||
type: 'php',
|
||||
domain: '',
|
||||
aliases: [],
|
||||
php_version: '',
|
||||
source_control: '',
|
||||
user: '',
|
||||
});
|
||||
|
||||
const submit: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
form.post(route('sites.store', { server: form.data.server }));
|
||||
};
|
||||
|
||||
const getFormField = (field: DynamicFieldConfig) => {
|
||||
if (field.name === 'source_control') {
|
||||
return (
|
||||
<FormField key={`field-${field.name}`}>
|
||||
<Label htmlFor="source_control">Source Control</Label>
|
||||
<SourceControlSelect
|
||||
id="source_control"
|
||||
value={form.data.source_control}
|
||||
onValueChange={(value) => form.setData('source_control', value)}
|
||||
/>
|
||||
<InputError message={form.errors.source_control} />
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.name === 'php_version') {
|
||||
return (
|
||||
<FormField key={`field-${field.name}`}>
|
||||
<Label htmlFor="php_version">PHP Version</Label>
|
||||
<ServiceVersionSelect
|
||||
id="php_version"
|
||||
serverId={parseInt(form.data.server)}
|
||||
service="php"
|
||||
value={form.data.php_version}
|
||||
onValueChange={(value) => form.setData('php_version', value)}
|
||||
/>
|
||||
<InputError message={form.errors.php_version} />
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DynamicField
|
||||
key={`field-${field.name}`}
|
||||
/*@ts-expect-error dynamic types*/
|
||||
value={form.data[field.name]}
|
||||
/*@ts-expect-error dynamic types*/
|
||||
onChange={(value) => form.setData(field.name, value)}
|
||||
config={field}
|
||||
/*@ts-expect-error dynamic types*/
|
||||
error={form.errors[field.name]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>{children}</SheetTrigger>
|
||||
<SheetContent className="w-full lg:max-w-3xl">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Create site</SheetTitle>
|
||||
<SheetDescription>Fill in the details to create a new site.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<Form id="create-site-form" className="p-4" onSubmit={submit}>
|
||||
<FormFields>
|
||||
{server === undefined && (
|
||||
<FormField>
|
||||
<Label htmlFor="server">Server</Label>
|
||||
<ServerSelect value={form.data.server} onValueChange={(value) => form.setData('server', value.id.toString())} />
|
||||
<InputError message={form.errors.server} />
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
{form.data.server && (
|
||||
<>
|
||||
<FormField>
|
||||
<Label htmlFor="type">Site Type</Label>
|
||||
<Select value={form.data.type} onValueChange={(value) => form.setData('type', value)}>
|
||||
<SelectTrigger id="type">
|
||||
<SelectValue placeholder="Select site type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{page.props.configs.site_types.map((type) => (
|
||||
<SelectItem key={`type-${type}`} value={type}>
|
||||
{type}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={form.errors.type} />
|
||||
</FormField>
|
||||
|
||||
<FormField>
|
||||
<Label htmlFor="domain">Domain</Label>
|
||||
<Input
|
||||
id="domain"
|
||||
type="text"
|
||||
value={form.data.domain}
|
||||
onChange={(e) => form.setData('domain', e.target.value)}
|
||||
placeholder="vitodeploy.com"
|
||||
/>
|
||||
<InputError message={form.errors.domain} />
|
||||
</FormField>
|
||||
|
||||
<FormField>
|
||||
<Label htmlFor="aliases">Aliases</Label>
|
||||
<TagsInput
|
||||
id="aliases"
|
||||
type="text"
|
||||
value={form.data.aliases}
|
||||
placeholder="Add aliases"
|
||||
onValueChange={(value) => form.setData('aliases', value)}
|
||||
/>
|
||||
<InputError message={form.errors.aliases} />
|
||||
</FormField>
|
||||
|
||||
{page.props.configs.site_types_custom_fields[form.data.type].map((config) => getFormField(config))}
|
||||
|
||||
<FormField>
|
||||
<Label htmlFor="user">Isolated User (Optional)</Label>
|
||||
<Input
|
||||
id="user"
|
||||
type="text"
|
||||
value={form.data.user}
|
||||
onChange={(e) => form.setData('user', e.target.value)}
|
||||
placeholder="Leave empty for using server's default user"
|
||||
/>
|
||||
<InputError message={form.errors.user} />
|
||||
</FormField>
|
||||
</>
|
||||
)}
|
||||
</FormFields>
|
||||
</Form>
|
||||
<SheetFooter>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="submit" form="create-site-form" disabled={form.processing}>
|
||||
{form.processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />} Create
|
||||
</Button>
|
||||
<SheetClose asChild>
|
||||
<Button variant="outline" disabled={form.processing}>
|
||||
Cancel
|
||||
</Button>
|
||||
</SheetClose>
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
52
resources/js/pages/sites/index.tsx
Normal file
52
resources/js/pages/sites/index.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { Head, usePage } from '@inertiajs/react';
|
||||
import { Server } from '@/types/server';
|
||||
import { Site } from '@/types/site';
|
||||
import ServerLayout from '@/layouts/server/layout';
|
||||
import Layout from '@/layouts/app/layout';
|
||||
import Container from '@/components/container';
|
||||
import HeaderContainer from '@/components/header-container';
|
||||
import Heading from '@/components/heading';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { BookOpenIcon, PlusIcon } from 'lucide-react';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import getColumns from '@/pages/sites/components/columns';
|
||||
import { PaginatedData } from '@/types';
|
||||
import CreateSite from '@/pages/sites/components/create-site';
|
||||
|
||||
type Page = {
|
||||
server?: Server;
|
||||
sites: PaginatedData<Site>;
|
||||
};
|
||||
|
||||
export default function Sites() {
|
||||
const page = usePage<Page>();
|
||||
|
||||
const Comp = page.props.server ? ServerLayout : Layout;
|
||||
|
||||
return (
|
||||
<Comp>
|
||||
<Head title={`Sites ${page.props.server ? ' - ' + page.props.server.name : ''}`} />
|
||||
<Container className="max-w-5xl">
|
||||
<HeaderContainer>
|
||||
<Heading title="Sites" description="Here you can manage websites" />
|
||||
<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>
|
||||
<CreateSite server={page.props.server}>
|
||||
<Button>
|
||||
<PlusIcon />
|
||||
<span className="hidden lg:block">Create site</span>
|
||||
</Button>
|
||||
</CreateSite>
|
||||
</div>
|
||||
</HeaderContainer>
|
||||
|
||||
<DataTable columns={getColumns(page.props.server)} paginatedData={page.props.sites} />
|
||||
</Container>
|
||||
</Comp>
|
||||
);
|
||||
}
|
46
resources/js/pages/sites/show.tsx
Normal file
46
resources/js/pages/sites/show.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
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 } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { PaginatedData } from '@/types';
|
||||
import { ServerLog } from '@/types/server-log';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { columns } from '@/pages/server-logs/components/columns';
|
||||
|
||||
type Page = {
|
||||
server: Server;
|
||||
site: Site;
|
||||
logs: PaginatedData<ServerLog>;
|
||||
};
|
||||
|
||||
export default function ShowSite() {
|
||||
const page = usePage<Page>();
|
||||
|
||||
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>
|
||||
</div>
|
||||
</HeaderContainer>
|
||||
|
||||
<DataTable columns={columns} paginatedData={page.props.logs} />
|
||||
</Container>
|
||||
</ServerLayout>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user