-
Executed Tasks
-
Showing total executed tasks for specified time range
+
{t('Executed Tasks')}
+
{t('Showing total executed tasks for specified time range')}
-
),
cell: ({ row }) => {
- return 'project' in row.original.data ? (
-
- {row.original.data.project?.displayName}
-
+ return row.original.projectId &&
+ 'project' in row.original.data ? (
+
+
+ {row.original.data.project?.displayName}
+
+
) : (
{t('N/A')}
);
diff --git a/packages/react-ui/src/app/routes/platform/setup/license-key/index.tsx b/packages/react-ui/src/app/routes/platform/setup/license-key/index.tsx
index 2a5b51e096..007db4f4d3 100644
--- a/packages/react-ui/src/app/routes/platform/setup/license-key/index.tsx
+++ b/packages/react-ui/src/app/routes/platform/setup/license-key/index.tsx
@@ -34,7 +34,7 @@ import { ApEdition, ApFlagId, isNil } from '@activepieces/shared';
import { ActivateLicenseDialog } from './activate-license-dialog';
const LICENSE_PROPS_MAP = {
- gitSyncEnabled: 'Team Collaboration via Git',
+ environmentEnabled: 'Team Collaboration via Git',
analyticsEnabled: 'Analytics',
auditLogEnabled: 'Audit Log',
embeddingEnabled: 'Embedding',
diff --git a/packages/react-ui/src/app/routes/project-release/apply-plan.tsx b/packages/react-ui/src/app/routes/project-release/apply-plan.tsx
new file mode 100644
index 0000000000..990954f505
--- /dev/null
+++ b/packages/react-ui/src/app/routes/project-release/apply-plan.tsx
@@ -0,0 +1,106 @@
+import { useMutation } from '@tanstack/react-query';
+import { t } from 'i18next';
+import { useState, ReactNode } from 'react';
+
+import { Button, ButtonProps } from '@/components/ui/button';
+import { INTERNAL_ERROR_TOAST, useToast } from '@/components/ui/use-toast';
+import { ConnectGitDialog } from '@/features/git-sync/components/connect-git-dialog';
+import { gitSyncHooks } from '@/features/git-sync/lib/git-sync-hooks';
+import { projectReleaseApi } from '@/features/project-version/lib/project-release-api';
+import { authenticationSession } from '@/lib/authentication-session';
+import {
+ DiffReleaseRequest,
+ isNil,
+ ProjectReleaseType,
+} from '@activepieces/shared';
+
+import { CreateReleaseDialog } from './create-release-dialog';
+
+type ApplyButtonProps = ButtonProps & {
+ request: DiffReleaseRequest;
+ children: ReactNode;
+ onSuccess: () => void;
+ defaultName?: string;
+};
+
+export const ApplyButton = ({
+ request,
+ children,
+ onSuccess,
+ defaultName,
+ ...props
+}: ApplyButtonProps) => {
+ const { toast } = useToast();
+ const projectId = authenticationSession.getProjectId()!;
+ const { gitSync } = gitSyncHooks.useGitSync(projectId, !isNil(projectId));
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const [syncPlan, setSyncPlan] = useState
(null);
+ const [loadingRequestId, setLoadingRequestId] = useState(null);
+
+ const { mutate: loadSyncPlan } = useMutation({
+ mutationFn: (request: DiffReleaseRequest) =>
+ projectReleaseApi.diff(request),
+ onSuccess: (plan) => {
+ if (!plan.operations || plan.operations.length === 0) {
+ toast({
+ title: t('No Changes Found'),
+ description: t('There are no differences to apply'),
+ variant: 'default',
+ });
+ setLoadingRequestId(null);
+ return;
+ }
+ setSyncPlan(plan);
+ setDialogOpen(true);
+ setLoadingRequestId(null);
+ },
+ onError: () => {
+ toast(INTERNAL_ERROR_TOAST);
+ setLoadingRequestId(null);
+ },
+ });
+
+ const [gitDialogOpen, setGitDialogOpen] = useState(false);
+ const showGitDialog =
+ isNil(gitSync) && request.type === ProjectReleaseType.GIT;
+ const requestId = JSON.stringify(request);
+ const isLoading = loadingRequestId === requestId;
+
+ return (
+ <>
+ {
+ if (showGitDialog) {
+ setGitDialogOpen(true);
+ } else {
+ setLoadingRequestId(requestId);
+ loadSyncPlan(request);
+ }
+ }}
+ >
+ {children}
+
+
+ {gitDialogOpen ? (
+
+ ) : (
+ dialogOpen && (
+
+ )
+ )}
+ >
+ );
+};
diff --git a/packages/react-ui/src/app/routes/project-release/create-release-dialog/index.tsx b/packages/react-ui/src/app/routes/project-release/create-release-dialog/index.tsx
new file mode 100644
index 0000000000..713653abc8
--- /dev/null
+++ b/packages/react-ui/src/app/routes/project-release/create-release-dialog/index.tsx
@@ -0,0 +1,246 @@
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useMutation } from '@tanstack/react-query';
+import { t } from 'i18next';
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import * as z from 'zod';
+
+import { Button } from '@/components/ui/button';
+import { Checkbox } from '@/components/ui/checkbox';
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Textarea } from '@/components/ui/textarea';
+import { INTERNAL_ERROR_TOAST, toast } from '@/components/ui/use-toast';
+import { gitSyncHooks } from '@/features/git-sync/lib/git-sync-hooks';
+import { projectReleaseApi } from '@/features/project-version/lib/project-release-api';
+import { platformHooks } from '@/hooks/platform-hooks';
+import { authenticationSession } from '@/lib/authentication-session';
+import { ProjectSyncPlan } from '@activepieces/ee-shared';
+import { DiffReleaseRequest, ProjectReleaseType } from '@activepieces/shared';
+
+import { OperationChange } from './operation-change';
+
+type CreateReleaseDialogProps = {
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ refetch: () => void;
+ diffRequest: DiffReleaseRequest;
+ plan: ProjectSyncPlan | undefined;
+ defaultName?: string;
+};
+
+const formSchema = z.object({
+ name: z.string().min(1, t('Name is required')),
+ description: z.string(),
+});
+
+type FormData = z.infer;
+
+const CreateReleaseDialog = ({
+ open,
+ setOpen,
+ refetch,
+ plan,
+ defaultName = '',
+ diffRequest,
+}: CreateReleaseDialogProps) => {
+ const { platform } = platformHooks.useCurrentPlatform();
+ const { gitSync } = gitSyncHooks.useGitSync(
+ authenticationSession.getProjectId()!,
+ platform.environmentsEnabled,
+ );
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ name: defaultName,
+ description: '',
+ },
+ });
+
+ const { mutate: applyChanges, isPending } = useMutation({
+ mutationFn: async () => {
+ switch (diffRequest.type) {
+ case ProjectReleaseType.GIT:
+ if (!gitSync) {
+ throw new Error('Git sync is not connected');
+ }
+ await projectReleaseApi.create({
+ name: form.getValues('name'),
+ description: form.getValues('description'),
+ selectedFlowsIds: Array.from(selectedChanges),
+ repoId: gitSync.id,
+ type: diffRequest.type,
+ });
+ break;
+ case ProjectReleaseType.PROJECT:
+ if (!diffRequest.targetProjectId) {
+ throw new Error('Project ID is required');
+ }
+ await projectReleaseApi.create({
+ name: form.getValues('name'),
+ description: form.getValues('description'),
+ selectedFlowsIds: Array.from(selectedChanges),
+ targetProjectId: diffRequest.targetProjectId,
+ type: diffRequest.type,
+ });
+ break;
+ case ProjectReleaseType.ROLLBACK:
+ await projectReleaseApi.create({
+ name: form.getValues('name'),
+ description: form.getValues('description'),
+ selectedFlowsIds: Array.from(selectedChanges),
+ projectReleaseId: diffRequest.projectReleaseId,
+ type: diffRequest.type,
+ });
+ break;
+ }
+ },
+ onSuccess: () => {
+ refetch();
+ setOpen(false);
+ },
+ onError: (error) => {
+ console.error(error);
+ toast(INTERNAL_ERROR_TOAST);
+ },
+ });
+
+ const [selectedChanges, setSelectedChanges] = useState>(
+ new Set(plan?.operations.map((op) => op.flow.id) || []),
+ );
+
+ const handleSelectAll = (checked: boolean) => {
+ if (!plan) return;
+ setSelectedChanges(
+ new Set(checked ? plan.operations.map((op) => op.flow.id) : []),
+ );
+ };
+
+ return (
+ {
+ if (newOpenState) {
+ form.reset({
+ name: '',
+ description: '',
+ });
+ }
+ setOpen(newOpenState);
+ }}
+ >
+
+
+
+ {diffRequest.type === ProjectReleaseType.GIT
+ ? t('Create Git Release')
+ : diffRequest.type === ProjectReleaseType.PROJECT
+ ? t('Create Project Release')
+ : t('Rollback Release')}
+
+
+
+
+
+ {t('Name')}
+
+
+ {form.formState.errors.name && (
+
+ {form.formState.errors.name.message}
+
+ )}
+
+
+
+ {t('Description')}
+
+
+ {form.formState.errors.description && (
+
+ {form.formState.errors.description.message}
+
+ )}
+
+
+
+
+
+
+
+ {t('Changes')} ({selectedChanges.size}/
+ {plan?.operations.length || 0})
+
+
+
+ {plan?.operations.length ? (
+ plan.operations.map((operation) => (
+
{
+ setSelectedChanges(
+ new Set(
+ checked
+ ? [...selectedChanges, operation.flow.id]
+ : [...selectedChanges].filter(
+ (id) => id !== operation.flow.id,
+ ),
+ ),
+ );
+ }}
+ />
+ ))
+ ) : (
+
+ {t('No changes to apply')}
+
+ )}
+
+
+
+
+ setOpen(false)}
+ >
+ {t('Cancel')}
+
+ {
+ applyChanges();
+ }}
+ >
+ {t('Apply Changes')}
+
+
+
+
+ );
+};
+
+export { CreateReleaseDialog };
diff --git a/packages/react-ui/src/app/routes/project-release/create-release-dialog/operation-change.tsx b/packages/react-ui/src/app/routes/project-release/create-release-dialog/operation-change.tsx
new file mode 100644
index 0000000000..07f3a89fb4
--- /dev/null
+++ b/packages/react-ui/src/app/routes/project-release/create-release-dialog/operation-change.tsx
@@ -0,0 +1,61 @@
+import { UpdateIcon } from '@radix-ui/react-icons';
+import { Minus, Plus } from 'lucide-react';
+import React from 'react';
+
+import { Checkbox } from '@/components/ui/checkbox';
+import {
+ ProjectOperationType,
+ ProjectSyncPlanOperation,
+} from '@activepieces/ee-shared';
+
+const renderDiffInfo = (flowName: string, icon: React.ReactNode) => (
+
+
+ {icon}
+ {flowName}
+
+
+);
+
+type OperationChangeProps = {
+ change: ProjectSyncPlanOperation;
+ selected: boolean;
+ onSelect: (selected: boolean) => void;
+};
+
+export const OperationChange = React.memo(
+ ({ change, selected, onSelect }: OperationChangeProps) => {
+ return (
+
+ {change.type === ProjectOperationType.CREATE_FLOW && (
+
+
+ {renderDiffInfo(
+ change.flow.displayName,
+
,
+ )}
+
+ )}
+ {change.type === ProjectOperationType.UPDATE_FLOW && (
+
+
+ {renderDiffInfo(
+ change.targetFlow.displayName,
+ ,
+ )}
+
+ )}
+ {change.type === ProjectOperationType.DELETE_FLOW && (
+
+
+ {renderDiffInfo(
+ change.flow.displayName,
+ ,
+ )}
+
+ )}
+
+ );
+ },
+);
+OperationChange.displayName = 'OperationChange';
diff --git a/packages/react-ui/src/app/routes/project-release/download-button.tsx b/packages/react-ui/src/app/routes/project-release/download-button.tsx
new file mode 100644
index 0000000000..c749483472
--- /dev/null
+++ b/packages/react-ui/src/app/routes/project-release/download-button.tsx
@@ -0,0 +1,57 @@
+import { useMutation } from '@tanstack/react-query';
+import { t } from 'i18next';
+import { DownloadIcon } from 'lucide-react';
+
+import { Button } from '@/components/ui/button';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+import { INTERNAL_ERROR_TOAST, useToast } from '@/components/ui/use-toast';
+import { projectReleaseApi } from '@/features/project-version/lib/project-release-api';
+import { ProjectRelease } from '@activepieces/shared';
+
+export const DownloadButton = ({ release }: { release: ProjectRelease }) => {
+ const { toast } = useToast();
+ const { mutate: downloadProjectRelease, isPending: isDownloading } =
+ useMutation({
+ mutationFn: async ({ releaseId }: { releaseId: string }) => {
+ return await projectReleaseApi.export(releaseId);
+ },
+ onSuccess: (data) => {
+ const blob = new Blob([JSON.stringify(data)], {
+ type: 'application/json',
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${release.name || 'release'}.json`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ },
+ onError: () => {
+ toast(INTERNAL_ERROR_TOAST);
+ },
+ });
+
+ return (
+
+
+
+ downloadProjectRelease({ releaseId: release.id })}
+ >
+
+
+
+ {t('Download')}
+
+
+ );
+};
diff --git a/packages/react-ui/src/app/routes/project-release/index.tsx b/packages/react-ui/src/app/routes/project-release/index.tsx
new file mode 100644
index 0000000000..320787d1c7
--- /dev/null
+++ b/packages/react-ui/src/app/routes/project-release/index.tsx
@@ -0,0 +1,186 @@
+import { useQuery } from '@tanstack/react-query';
+import { ColumnDef } from '@tanstack/react-table';
+import { t } from 'i18next';
+import {
+ Plus,
+ ChevronDown,
+ Undo2,
+ GitBranch,
+ RotateCcw,
+ FolderOpenDot,
+} from 'lucide-react';
+
+import { Button } from '@/components/ui/button';
+import { DataTable, RowDataWithActions } from '@/components/ui/data-table';
+import { DataTableColumnHeader } from '@/components/ui/data-table/data-table-column-header';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { TableTitle } from '@/components/ui/table-title';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+import { projectReleaseApi } from '@/features/project-version/lib/project-release-api';
+import { formatUtils } from '@/lib/utils';
+import { ProjectRelease, ProjectReleaseType } from '@activepieces/shared';
+
+import { ApplyButton } from './apply-plan';
+import { DownloadButton } from './download-button';
+import { SelectionButton } from './selection-dialog';
+
+const ProjectReleasesPage = () => {
+ const { data, isLoading, refetch } = useQuery({
+ queryKey: ['project-releases'],
+ queryFn: () => projectReleaseApi.list(),
+ });
+
+ const columns: ColumnDef>[] = [
+ {
+ accessorKey: 'name',
+ accessorFn: (row) => row.name,
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {row.original.name}
,
+ },
+ {
+ accessorKey: 'type',
+ accessorFn: (row) => row.type,
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const isGit = row.original.type === ProjectReleaseType.GIT;
+ const isProject = row.original.type === ProjectReleaseType.PROJECT;
+ return (
+
+ {isGit ? (
+
+ ) : isProject ? (
+
+ ) : (
+
+ )}
+ {isGit ? 'Git' : isProject ? 'Project' : 'Rollback'}
+
+ );
+ },
+ },
+ {
+ accessorKey: 'created',
+ accessorFn: (row) => row.created,
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => (
+
+ {formatUtils.formatDate(new Date(row.original.created))}
+
+ ),
+ },
+ {
+ accessorKey: 'importedBy',
+ accessorFn: (row) => row.importedBy,
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => (
+ {row.original.importedByUser?.email}
+ ),
+ },
+ ];
+
+ return (
+
+
+
+
{t('Project Releases')}
+
+ {t('Track and manage your project version history and deployments')}
+
+
+
+
+
+
+ {t('Create Release')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{t('From Project')}
+
+
+
+
+
+
+
+
,
+ (row) => {
+ return (
+
+
+
+
+
+
+
+ {t('Rollback')}
+
+
+ );
+ },
+ ]}
+ />
+
+ );
+};
+
+ProjectReleasesPage.displayName = 'ProjectReleasesPage';
+export { ProjectReleasesPage };
diff --git a/packages/react-ui/src/app/routes/project-release/selection-dialog.tsx b/packages/react-ui/src/app/routes/project-release/selection-dialog.tsx
new file mode 100644
index 0000000000..a0008efa26
--- /dev/null
+++ b/packages/react-ui/src/app/routes/project-release/selection-dialog.tsx
@@ -0,0 +1,45 @@
+import { ReactNode, useState } from 'react';
+
+import { Button, ButtonProps } from '@/components/ui/button';
+import { projectHooks } from '@/hooks/project-hooks';
+import { ProjectReleaseType } from '@activepieces/shared';
+
+import { ProjectSelectionDialog } from './selection-release-dialog/project-dialog';
+
+type SelectionButtonProps = ButtonProps & {
+ ReleaseType: ProjectReleaseType;
+ children: ReactNode;
+ onSuccess: () => void;
+ defaultName?: string;
+};
+export function SelectionButton({
+ ReleaseType,
+ children,
+ onSuccess,
+ defaultName,
+ ...props
+}: SelectionButtonProps) {
+ const { project } = projectHooks.useCurrentProject();
+ const [open, setOpen] = useState(false);
+
+ return (
+ <>
+ {
+ setOpen(true);
+ }}
+ >
+ {children}
+
+ {ReleaseType === ProjectReleaseType.PROJECT && (
+
+ )}
+ >
+ );
+}
diff --git a/packages/react-ui/src/app/routes/project-release/selection-release-dialog/project-dialog.tsx b/packages/react-ui/src/app/routes/project-release/selection-release-dialog/project-dialog.tsx
new file mode 100644
index 0000000000..1c3b359bf3
--- /dev/null
+++ b/packages/react-ui/src/app/routes/project-release/selection-release-dialog/project-dialog.tsx
@@ -0,0 +1,190 @@
+import { typeboxResolver } from '@hookform/resolvers/typebox';
+import { Static, Type } from '@sinclair/typebox';
+import { useMutation } from '@tanstack/react-query';
+import { t } from 'i18next';
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+
+import { Button } from '@/components/ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { FormField, FormItem, Form, FormMessage } from '@/components/ui/form';
+import { Label } from '@/components/ui/label';
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { INTERNAL_ERROR_TOAST, toast } from '@/components/ui/use-toast';
+import { projectReleaseApi } from '@/features/project-version/lib/project-release-api';
+import { projectHooks } from '@/hooks/project-hooks';
+import { DiffReleaseRequest, ProjectReleaseType } from '@activepieces/shared';
+
+import { CreateReleaseDialog } from '../create-release-dialog';
+
+const FormSchema = Type.Object({
+ selectedProject: Type.String({
+ errorMessage: t('Please select project'),
+ required: true,
+ }),
+});
+
+type FormSchema = Static;
+
+type ProjectSelectionDialogProps = {
+ projectId: string;
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ onSuccess: () => void;
+};
+
+export function ProjectSelectionDialog({
+ projectId,
+ open,
+ setOpen,
+ onSuccess,
+}: ProjectSelectionDialogProps) {
+ const { data: projects } = projectHooks.useProjects();
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const [syncPlan, setSyncPlan] = useState(null);
+ const [loading, setLoading] = useState(false);
+
+ const { mutate: loadSyncPlan } = useMutation({
+ mutationFn: (request: DiffReleaseRequest) =>
+ projectReleaseApi.diff(request),
+ onSuccess: (plan) => {
+ if (!plan.operations || plan.operations.length === 0) {
+ toast({
+ title: t('No Changes Found'),
+ description: t('There are no differences to apply'),
+ variant: 'default',
+ });
+ setLoading(false);
+ return;
+ }
+ setSyncPlan(plan);
+ setLoading(false);
+ setOpen(false);
+ setDialogOpen(true);
+ },
+ onError: () => {
+ toast(INTERNAL_ERROR_TOAST);
+ setLoading(false);
+ },
+ });
+
+ const form = useForm({
+ resolver: typeboxResolver(FormSchema),
+ defaultValues: {
+ selectedProject: projects?.find((project) => project.id !== projectId)
+ ?.id,
+ },
+ });
+ const onSubmit = (data: FormSchema) => {
+ if (!data.selectedProject) {
+ form.setError('selectedProject', {
+ type: 'required',
+ message: t('Please select a project'),
+ });
+ return;
+ }
+ setLoading(true);
+ loadSyncPlan({
+ type: ProjectReleaseType.PROJECT,
+ targetProjectId: data.selectedProject,
+ });
+ };
+
+ return (
+ <>
+ {
+ setOpen(open);
+ if (open) {
+ form.reset();
+ }
+ }}
+ >
+
+
+ {t('Create Release')}
+
+
+
+
+
+
+ {dialogOpen && (
+
+ )}
+ >
+ );
+}
diff --git a/packages/react-ui/src/app/routes/settings/git-sync/index.tsx b/packages/react-ui/src/app/routes/settings/environment/index.tsx
similarity index 77%
rename from packages/react-ui/src/app/routes/settings/git-sync/index.tsx
rename to packages/react-ui/src/app/routes/settings/environment/index.tsx
index d458115ce0..36f91082a0 100644
--- a/packages/react-ui/src/app/routes/settings/git-sync/index.tsx
+++ b/packages/react-ui/src/app/routes/settings/environment/index.tsx
@@ -7,19 +7,20 @@ import { Card } from '@/components/ui/card';
import { LoadingSpinner } from '@/components/ui/spinner';
import { INTERNAL_ERROR_TOAST, toast } from '@/components/ui/use-toast';
import { ConnectGitDialog } from '@/features/git-sync/components/connect-git-dialog';
-import { ReviewChangeDialog } from '@/features/git-sync/components/review-change-dialog';
import { gitSyncApi } from '@/features/git-sync/lib/git-sync-api';
import { gitSyncHooks } from '@/features/git-sync/lib/git-sync-hooks';
import { platformHooks } from '@/hooks/platform-hooks';
import { authenticationSession } from '@/lib/authentication-session';
import { assertNotNullOrUndefined } from '@activepieces/shared';
-const GitSyncPage = () => {
+import { ReleaseCard } from './release-card';
+
+const EnvironmentPage = () => {
const { platform } = platformHooks.useCurrentPlatform();
const { gitSync, isLoading, refetch } = gitSyncHooks.useGitSync(
authenticationSession.getProjectId()!,
- platform.gitSyncEnabled,
+ platform.environmentsEnabled,
);
const { mutate } = useMutation({
@@ -42,16 +43,16 @@ const GitSyncPage = () => {
return (
-
{t('Git Sync')}
+ {t('Environment')}
{t(
'This feature allows for the creation of an external backup, environments, and maintaining a version history',
@@ -75,16 +76,20 @@ const GitSyncPage = () => {
- {!gitSync &&
}
+ {!gitSync && (
+
+ )}
{gitSync && (
-
mutate()}
- className="w-32 text-destructive"
- variant={'basic'}
- >
- {t('Disconnect')}
-
+
+ mutate()}
+ className="w-32 text-destructive"
+ variant={'basic'}
+ >
+ {t('Disconnect')}
+
+
)}
>
@@ -96,14 +101,10 @@ const GitSyncPage = () => {
)}
-
- {gitSync && (
-
- )}
-
+
);
};
-export { GitSyncPage };
+export { EnvironmentPage };
diff --git a/packages/react-ui/src/app/routes/settings/environment/release-card.tsx b/packages/react-ui/src/app/routes/settings/environment/release-card.tsx
new file mode 100644
index 0000000000..2df1a79836
--- /dev/null
+++ b/packages/react-ui/src/app/routes/settings/environment/release-card.tsx
@@ -0,0 +1,63 @@
+import { useMutation } from '@tanstack/react-query';
+import { t } from 'i18next';
+import { Package } from 'lucide-react';
+
+import { Button } from '@/components/ui/button';
+import { Card } from '@/components/ui/card';
+import { toast, INTERNAL_ERROR_TOAST } from '@/components/ui/use-toast';
+import { projectHooks } from '@/hooks/project-hooks';
+import { projectApi } from '@/lib/project-api';
+import { cn } from '@/lib/utils';
+
+const ReleaseCard = () => {
+ const { project, refetch } = projectHooks.useCurrentProject();
+
+ const { mutate } = useMutation({
+ mutationFn: () => {
+ return projectApi.update(project.id, {
+ releasesEnabled: !project.releasesEnabled,
+ });
+ },
+ onSuccess: () => {
+ refetch();
+ toast({
+ title: t('Releases Enabled'),
+ description: t('You have successfully enabled releases'),
+ duration: 3000,
+ });
+ },
+ onError: () => {
+ toast(INTERNAL_ERROR_TOAST);
+ },
+ });
+
+ return (
+