Plugins base (#613)

* wip

* wip

* cleanup

* notification channels

* phpstan

* services

* remove server types

* refactoring

* refactoring
This commit is contained in:
Saeed Vaziry
2025-06-14 14:35:18 +02:00
committed by GitHub
parent adc0653d15
commit 131b828807
311 changed files with 3976 additions and 2660 deletions

View File

@ -0,0 +1,94 @@
import { Site, SiteFeatureAction } from '@/types/site';
import React, { FormEvent, ReactNode, useState } from 'react';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Form, FormField, FormFields } from '@/components/ui/form';
import { Button } from '@/components/ui/button';
import { useForm } from '@inertiajs/react';
import { DynamicFieldConfig } from '@/types/dynamic-field-config';
import DynamicField from '@/components/ui/dynamic-field';
import { LoaderCircleIcon } from 'lucide-react';
export default function FeatureAction({
site,
featureId,
actionId,
action,
children,
}: {
site: Site;
featureId: string;
actionId: string;
action: SiteFeatureAction;
children: ReactNode;
}) {
const [open, setOpen] = useState(false);
const form = useForm();
const submit = (e: FormEvent) => {
e.preventDefault();
form.post(
route('site-features.action', {
server: site.server_id,
site: site.id,
feature: featureId,
action: actionId,
}),
{
onSuccess: () => {
setOpen(false);
form.reset();
},
},
);
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>{action.label}</DialogTitle>
<DialogDescription className="sr-only">action {action.label}</DialogDescription>
</DialogHeader>
<Form id="action-form" onSubmit={submit} className="p-4">
<FormFields>
<FormField>
<p className="text-muted-foreground">
You're performing action <b className="text-foreground">[{action.label}]</b> on site{' '}
<b className="text-foreground">[{site.domain}]</b>
</p>
</FormField>
{action.form?.map((field: DynamicFieldConfig) => (
<DynamicField
key={`field-${field.name}`}
/*@ts-expect-error dynamic types*/
value={form.data[field.name]}
onChange={(value) => form.setData(field.name, value)}
config={field}
error={form.errors[field.name]}
/>
))}
</FormFields>
</Form>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button form="action-form" disabled={form.processing} onClick={submit}>
{form.processing && <LoaderCircleIcon className="animate-spin" />}
{action.label}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,83 @@
import { Head, usePage } from '@inertiajs/react';
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 ServerLayout from '@/layouts/server/layout';
import { BookOpenIcon, MoreVerticalIcon } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import React from 'react';
import { Site, SiteFeature } from '@/types/site';
import { Separator } from '@/components/ui/separator';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import FeatureAction from '@/pages/site-features/components/feature-action';
export default function SiteFeatures() {
const page = usePage<{
server: Server;
site: Site;
features: {
[key: string]: SiteFeature;
};
}>();
return (
<ServerLayout>
<Head title={`Features - ${page.props.site.domain}`} />
<Container className="max-w-5xl">
<HeaderContainer>
<Heading title="Features" description="Your site has some features enabled by Vito or other plugins" />
<div className="flex items-center gap-2">
<a href="https://vitodeploy.com/docs/sites/features" target="_blank">
<Button variant="outline">
<BookOpenIcon />
<span className="hidden lg:block">Docs</span>
</Button>
</a>
</div>
</HeaderContainer>
<Card>
<CardHeader className="flex-row items-center justify-between gap-2">
<div className="space-y-2">
<CardTitle>Site features</CardTitle>
<CardDescription>Here you can see the list of features and their actions</CardDescription>
</div>
</CardHeader>
<CardContent>
{Object.entries(page.props.features).map(([key, feature], index) => (
<div key={`feature-${key}`}>
<div className="flex items-center justify-between p-4">
<div className="space-y-1">
<p>{feature.label}</p>
<p className="text-muted-foreground text-sm">{feature.description}</p>
</div>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button variant="outline">
Actions
<MoreVerticalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{Object.entries(feature.actions || {}).map(([actionKey, action]) => (
<FeatureAction key={`action-${actionKey}`} site={page.props.site} featureId={key} actionId={actionKey} action={action}>
<DropdownMenuItem onSelect={(e) => e.preventDefault()} disabled={!action.active}>
{action.label}
</DropdownMenuItem>
</FeatureAction>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
{index < Object.keys(page.props.features).length - 1 && <Separator />}
</div>
))}
</CardContent>
</Card>
</Container>
</ServerLayout>
);
}