index.tsx•8 kB
import { t } from 'i18next';
import { Bell, GitBranch, Puzzle, Settings, Users } from 'lucide-react';
import { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Avatar } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent } from '@/components/ui/dialog';
import { ScrollArea } from '@/components/ui/scroll-area';
import { LoadingSpinner } from '@/components/ui/spinner';
import { useAuthorization } from '@/hooks/authorization-hooks';
import { flagsHooks } from '@/hooks/flags-hooks';
import { projectHooks } from '@/hooks/project-hooks';
import { cn } from '@/lib/utils';
import {
ApFlagId,
isNil,
Permission,
PROJECT_COLOR_PALETTE,
} from '@activepieces/shared';
import { ApProjectDisplay } from '../ap-project-display';
import { AlertsSettings } from './alerts';
import { EnvironmentSettings } from './environment';
import { GeneralSettings, FormValues } from './general';
import { useGeneralSettingsMutation } from './general/hook';
import { PiecesSettings } from './pieces';
import { TeamSettings } from './team';
type TabId = 'general' | 'team' | 'alerts' | 'pieces' | 'environment';
interface ProjectSettingsDialogProps {
open: boolean;
onClose: () => void;
initialTab?: TabId;
initialValues?: {
projectName?: string;
aiCredits?: string;
externalId?: string;
};
}
export function ProjectSettingsDialog({
open,
onClose,
initialTab = 'general',
initialValues,
}: ProjectSettingsDialogProps) {
const [activeTab, setActiveTab] = useState<TabId>(initialTab);
const { checkAccess } = useAuthorization();
const { project } = projectHooks.useCurrentProject();
const { data: showAlerts } = flagsHooks.useFlag(ApFlagId.SHOW_ALERTS);
const { data: showProjectMembers } = flagsHooks.useFlag(
ApFlagId.SHOW_PROJECT_MEMBERS,
);
const form = useForm<FormValues>({
defaultValues: {
projectName: initialValues?.projectName,
icon: project.icon,
aiCredits: initialValues?.aiCredits || '',
externalId: initialValues?.externalId,
},
disabled: checkAccess(Permission.WRITE_PROJECT) === false,
});
const projectMutation = useGeneralSettingsMutation(project.id, form);
const handleSave = (values: FormValues) => {
projectMutation.mutate({
displayName: values.projectName,
icon: values.icon,
externalId: values.externalId,
plan: {
aiCredits: values.aiCredits ? parseInt(values.aiCredits) : undefined,
},
});
};
useEffect(() => {
if (open && !isNil(project)) {
form.reset({
...initialValues,
icon: project.icon,
});
setActiveTab(initialTab);
}
}, [open, project]);
const tabs = [
{
id: 'general' as TabId,
label: t('General'),
icon: <Settings className="w-4 h-4" />,
disabled: false,
},
{
id: 'team' as TabId,
label: t('Team'),
icon: <Users className="w-4 h-4" />,
disabled:
!checkAccess(Permission.READ_PROJECT_MEMBER) || !showProjectMembers,
},
{
id: 'alerts' as TabId,
label: t('Alerts'),
icon: <Bell className="w-4 h-4" />,
disabled: !checkAccess(Permission.READ_ALERT) || !showAlerts,
},
{
id: 'pieces' as TabId,
label: t('Pieces'),
icon: <Puzzle className="w-4 h-4" />,
disabled: false,
},
{
id: 'environment' as TabId,
label: t('Environment'),
icon: <GitBranch className="w-4 h-4" />,
disabled: !checkAccess(Permission.READ_PROJECT_RELEASE),
},
].filter((tab) => !tab.disabled);
const renderTabContent = () => {
switch (activeTab) {
case 'general':
return (
<GeneralSettings form={form} isSaving={projectMutation.isPending} />
);
case 'team':
return <TeamSettings />;
case 'alerts':
return <AlertsSettings />;
case 'pieces':
return <PiecesSettings />;
case 'environment':
return <EnvironmentSettings />;
default:
return null;
}
};
const renderTabHeader = () => {
return (
<span className="text-lg font-bold">
{tabs.find((tab) => tab.id === activeTab)?.label}
</span>
);
};
const renderDialogFooter = () => {
if (activeTab !== 'general') return null;
return (
<div className="border-t bg-background rounded-br-md">
<div className="flex items-center justify-end gap-3 px-6 py-4">
<Button
variant="outline"
size="sm"
onClick={onClose}
disabled={projectMutation.isPending}
>
{t('Close')}
</Button>
<Button
disabled={projectMutation.isPending}
size="sm"
onClick={form.handleSubmit(handleSave)}
>
{projectMutation.isPending ? (
<>
<LoadingSpinner className="w-4 h-4 mr-2" />
{t('Saving...')}
</>
) : (
t('Save Changes')
)}
</Button>
</div>
</div>
);
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-5xl w-full max-h-[95vh] rounded-sm flex flex-col p-0">
<div className="flex h-[700px]">
<div className="w-[238px]">
<nav className="bg-sidebar space-y-1 bg-muted rounded-sm rounded-r-none h-full flex flex-col rounded-l-md">
<ApProjectDisplay
title={project.displayName}
icon={project.icon}
containerClassName="px-3 my-4"
titleClassName="text-md font-bold"
maxLengthToNotShowTooltip={18}
/>
<div className="flex flex-col px-2 gap-1">
{tabs.map((tab) => (
<div
key={tab.id}
className={cn(
'flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm font-medium transition-all cursor-pointer hover:bg-sidebar-active',
{
'bg-sidebar-active': activeTab === tab.id,
},
)}
onClick={() => setActiveTab(tab.id)}
>
{tab.icon}
{tab.label}
</div>
))}
</div>
</nav>
</div>
<div className="flex-1 min-w-0 flex flex-col">
<div className="flex-1 min-h-0 overflow-hidden">
<ScrollArea className="h-full">
{activeTab === 'general' && (
<div
className="flex items-center justify-center w-full h-[114px] rounded-tr-md"
style={{
backgroundColor:
PROJECT_COLOR_PALETTE[project.icon.color].color + '26',
}}
>
<Avatar
className="h-[50px] w-[50px] flex items-center justify-center rounded-sm"
style={{
backgroundColor:
PROJECT_COLOR_PALETTE[project.icon.color].color,
color:
PROJECT_COLOR_PALETTE[project.icon.color].textColor,
}}
>
<span className="text-xl">
{project.displayName.charAt(0).toUpperCase()}
</span>
</Avatar>
</div>
)}
<div className="flex flex-col gap-3 px-10 pt-4">
{renderTabHeader()}
{renderTabContent()}
</div>
</ScrollArea>
</div>
{renderDialogFooter()}
</div>
</div>
</DialogContent>
</Dialog>
);
}