forked from cerc-io/snowballtools-base
Implement routes for project settings tab (#64)
* Add routes to settings tab panel * Refactor code to move settings tab components to pages * Rename registry fields in project and deployment entity * Use kebab case for routes --------- Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@ import { UseFormRegister } from 'react-hook-form';
|
||||
|
||||
import { Typography, Input, IconButton } from '@material-tailwind/react';
|
||||
|
||||
import { EnvironmentVariablesFormValues } from './EnvironmentVariablesTabPanel';
|
||||
import { EnvironmentVariablesFormValues } from '../../../../types/project';
|
||||
|
||||
interface AddEnvironmentVariableRowProps {
|
||||
onDelete: () => void;
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Domain, Project } from 'gql-client';
|
||||
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
import DomainCard from './DomainCard';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import repositories from '../../../../assets/repositories.json';
|
||||
|
||||
const Domains = ({ project }: { project: Project }) => {
|
||||
const client = useGQLClient();
|
||||
const [domains, setDomains] = useState<Domain[]>([]);
|
||||
|
||||
const fetchDomains = async () => {
|
||||
if (project === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchedDomains = await client.getDomains(project.id);
|
||||
setDomains(fetchedDomains.domains);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDomains();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between p-2">
|
||||
<Typography variant="h3">Domain</Typography>
|
||||
<Link to="domain/add">
|
||||
<Button color="blue" variant="outlined" className="rounded-full">
|
||||
<i>^</i> Add domain
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{domains.map((domain) => {
|
||||
return (
|
||||
<DomainCard
|
||||
domains={domains}
|
||||
domain={domain}
|
||||
key={domain.id}
|
||||
// TODO: Use github API for getting linked repository
|
||||
repo={repositories[0]!}
|
||||
project={project}
|
||||
onUpdate={fetchDomains}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Domains;
|
||||
-253
@@ -1,253 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Environment, EnvironmentVariable } from 'gql-client';
|
||||
|
||||
import {
|
||||
Typography,
|
||||
Collapse,
|
||||
Card,
|
||||
Button,
|
||||
Checkbox,
|
||||
Chip,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import AddEnvironmentVariableRow from './AddEnvironmentVariableRow';
|
||||
import DisplayEnvironmentVariables from './DisplayEnvironmentVariables';
|
||||
import HorizontalLine from '../../../HorizontalLine';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
|
||||
export type EnvironmentVariablesFormValues = {
|
||||
variables: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
environment: {
|
||||
development: boolean;
|
||||
preview: boolean;
|
||||
production: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export const EnvironmentVariablesTabPanel = () => {
|
||||
const { id } = useParams();
|
||||
const client = useGQLClient();
|
||||
|
||||
const [environmentVariables, setEnvironmentVariables] = useState<
|
||||
EnvironmentVariable[]
|
||||
>([]);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
control,
|
||||
reset,
|
||||
formState: { isSubmitSuccessful, errors },
|
||||
} = useForm<EnvironmentVariablesFormValues>({
|
||||
defaultValues: {
|
||||
variables: [{ key: '', value: '' }],
|
||||
environment: {
|
||||
development: false,
|
||||
preview: false,
|
||||
production: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
const [createNewVariable, setCreateNewVariable] = useState(false);
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: 'variables',
|
||||
control,
|
||||
rules: {
|
||||
required: 'Add at least 1 environment variables',
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitSuccessful) {
|
||||
reset();
|
||||
}
|
||||
}, [isSubmitSuccessful, reset, id]);
|
||||
|
||||
const getEnvironmentVariables = useCallback(
|
||||
(environment: Environment) => {
|
||||
return environmentVariables.filter(
|
||||
(item) => item.environment === environment,
|
||||
);
|
||||
},
|
||||
[environmentVariables, id],
|
||||
);
|
||||
|
||||
const isFieldEmpty = useMemo(() => {
|
||||
if (errors.variables) {
|
||||
return fields.some((_, index) => {
|
||||
if (
|
||||
errors.variables![index]?.value?.type === 'required' ||
|
||||
errors.variables![index]?.key?.type === 'required'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [fields, errors.variables, id]);
|
||||
|
||||
const fetchEnvironmentVariables = useCallback(
|
||||
async (id: string | undefined) => {
|
||||
if (id) {
|
||||
const { environmentVariables } =
|
||||
await client.getEnvironmentVariables(id);
|
||||
setEnvironmentVariables(environmentVariables);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchEnvironmentVariables(id);
|
||||
}, [id]);
|
||||
|
||||
const createEnvironmentVariablesHandler = useCallback(
|
||||
async (createFormData: EnvironmentVariablesFormValues) => {
|
||||
const environmentVariables = createFormData.variables.map((variable) => {
|
||||
return {
|
||||
key: variable.key,
|
||||
value: variable.value,
|
||||
environments: Object.entries(createFormData.environment)
|
||||
.filter(([, value]) => value === true)
|
||||
.map(([key]) => key.charAt(0).toUpperCase() + key.slice(1)),
|
||||
};
|
||||
});
|
||||
|
||||
const { addEnvironmentVariables: isEnvironmentVariablesAdded } =
|
||||
await client.addEnvironmentVariables(id!, environmentVariables);
|
||||
|
||||
if (isEnvironmentVariablesAdded) {
|
||||
reset();
|
||||
setCreateNewVariable((cur) => !cur);
|
||||
|
||||
fetchEnvironmentVariables(id);
|
||||
|
||||
toast.success(
|
||||
createFormData.variables.length > 1
|
||||
? `${createFormData.variables.length} variables added`
|
||||
: `Variable added`,
|
||||
);
|
||||
} else {
|
||||
toast.error('Environment variables not added');
|
||||
}
|
||||
},
|
||||
[id, client],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h6">Environment variables</Typography>
|
||||
<Typography variant="small" className="font-medium text-gray-800">
|
||||
A new deployment is required for your changes to take effect.
|
||||
</Typography>
|
||||
<div className="bg-gray-300 rounded-lg p-2">
|
||||
<div
|
||||
className="text-black"
|
||||
onClick={() => setCreateNewVariable((cur) => !cur)}
|
||||
>
|
||||
+ Create new variable
|
||||
</div>
|
||||
<Collapse open={createNewVariable}>
|
||||
<Card className="bg-white p-2">
|
||||
<form onSubmit={handleSubmit(createEnvironmentVariablesHandler)}>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<AddEnvironmentVariableRow
|
||||
key={field.id}
|
||||
index={index}
|
||||
register={register}
|
||||
onDelete={() => remove(index)}
|
||||
isDeleteDisabled={fields.length === 1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="flex gap-1 p-2">
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
append({
|
||||
key: '',
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
>
|
||||
+ Add variable
|
||||
</Button>
|
||||
<Button variant="outlined" size="sm">
|
||||
^ Import .env
|
||||
</Button>
|
||||
</div>
|
||||
{isFieldEmpty && (
|
||||
<Chip
|
||||
value="^ Please ensure no fields are empty before saving."
|
||||
variant="outlined"
|
||||
color="red"
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<Checkbox
|
||||
crossOrigin={undefined}
|
||||
label="Production"
|
||||
{...register(`environment.production`)}
|
||||
color="blue"
|
||||
/>
|
||||
<Checkbox
|
||||
crossOrigin={undefined}
|
||||
label="Preview"
|
||||
{...register(`environment.preview`)}
|
||||
color="blue"
|
||||
/>
|
||||
<Checkbox
|
||||
crossOrigin={undefined}
|
||||
label="Development"
|
||||
{...register(`environment.development`)}
|
||||
color="blue"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<Button size="lg" color="blue" type="submit">
|
||||
Save changes
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</Collapse>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<DisplayEnvironmentVariables
|
||||
environment={Environment.Production}
|
||||
variables={getEnvironmentVariables(Environment.Production)}
|
||||
onUpdate={async () => {
|
||||
await fetchEnvironmentVariables(id);
|
||||
}}
|
||||
/>
|
||||
<HorizontalLine />
|
||||
<DisplayEnvironmentVariables
|
||||
environment={Environment.Preview}
|
||||
variables={getEnvironmentVariables(Environment.Preview)}
|
||||
onUpdate={async () => {
|
||||
await fetchEnvironmentVariables(id);
|
||||
}}
|
||||
/>
|
||||
<HorizontalLine />
|
||||
<DisplayEnvironmentVariables
|
||||
environment={Environment.Development}
|
||||
variables={getEnvironmentVariables(Environment.Development)}
|
||||
onUpdate={async () => {
|
||||
await fetchEnvironmentVariables(id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,252 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Organization, Project } from 'gql-client';
|
||||
|
||||
import { Button, Typography, Input, Option } from '@material-tailwind/react';
|
||||
|
||||
import DeleteProjectDialog from './DeleteProjectDialog';
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import AsyncSelect from '../../../shared/AsyncSelect';
|
||||
|
||||
const CopyIcon = ({ value }: { value: string }) => {
|
||||
return (
|
||||
<span
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(value);
|
||||
toast.success('Project ID copied');
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
^
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const GeneralTabPanel = ({
|
||||
project,
|
||||
onUpdate,
|
||||
}: {
|
||||
project: Project;
|
||||
onUpdate: () => Promise<void>;
|
||||
}) => {
|
||||
const client = useGQLClient();
|
||||
const [transferOrganizations, setTransferOrganizations] = useState<
|
||||
Organization[]
|
||||
>([]);
|
||||
const [selectedTransferOrganization, setSelectedTransferOrganization] =
|
||||
useState('');
|
||||
|
||||
const {
|
||||
handleSubmit: handleTransfer,
|
||||
control,
|
||||
formState,
|
||||
reset: transferFormReset,
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
orgId: '',
|
||||
},
|
||||
});
|
||||
|
||||
const [openTransferDialog, setOpenTransferDialog] = useState(false);
|
||||
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
|
||||
|
||||
const handleDeleteProjectDialog = () =>
|
||||
setOpenDeleteDialog(!openDeleteDialog);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
reset,
|
||||
formState: updateProjectFormState,
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
appName: project.name,
|
||||
description: project.description,
|
||||
},
|
||||
});
|
||||
|
||||
const fetchUserOrganizations = useCallback(async () => {
|
||||
const { organizations } = await client.getOrganizations();
|
||||
const orgsToTransfer = organizations.filter(
|
||||
(org) => org.id !== project.organization.id,
|
||||
);
|
||||
setTransferOrganizations(orgsToTransfer);
|
||||
}, [project]);
|
||||
|
||||
const handleTransferProject = useCallback(async () => {
|
||||
const { updateProject: isTransferred } = await client.updateProject(
|
||||
project.id,
|
||||
{
|
||||
organizationId: selectedTransferOrganization,
|
||||
},
|
||||
);
|
||||
setOpenTransferDialog(!openTransferDialog);
|
||||
|
||||
if (isTransferred) {
|
||||
toast.success('Project transferred');
|
||||
await fetchUserOrganizations();
|
||||
await onUpdate();
|
||||
transferFormReset();
|
||||
} else {
|
||||
toast.error('Project not transrfered');
|
||||
}
|
||||
}, [project, selectedTransferOrganization]);
|
||||
|
||||
const selectedUserOrgName = useMemo(() => {
|
||||
return (
|
||||
transferOrganizations.find(
|
||||
(org) => org.id === selectedTransferOrganization,
|
||||
)?.name || ''
|
||||
);
|
||||
}, [transferOrganizations, selectedTransferOrganization]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserOrganizations();
|
||||
}, [project]);
|
||||
|
||||
useEffect(() => {
|
||||
reset({ appName: project.name, description: project.description });
|
||||
}, [project]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
onSubmit={handleSubmit(async ({ appName, description }) => {
|
||||
const { updateProject } = await client.updateProject(project.id, {
|
||||
name: appName,
|
||||
description,
|
||||
});
|
||||
if (updateProject) {
|
||||
await onUpdate();
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Typography variant="h6">Project info</Typography>
|
||||
<Typography variant="small" className="font-medium text-gray-800">
|
||||
App name
|
||||
</Typography>
|
||||
<Input
|
||||
variant="outlined"
|
||||
// TODO: Debug issue: https://github.com/creativetimofficial/material-tailwind/issues/427
|
||||
crossOrigin={undefined}
|
||||
size="md"
|
||||
{...register('appName')}
|
||||
/>
|
||||
<Typography variant="small" className="font-medium text-gray-800">
|
||||
Description (Optional)
|
||||
</Typography>
|
||||
<Input
|
||||
variant="outlined"
|
||||
crossOrigin={undefined}
|
||||
size="md"
|
||||
{...register('description')}
|
||||
/>
|
||||
<Typography variant="small" className="font-medium text-gray-800">
|
||||
Project ID
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
variant="outlined"
|
||||
value={project.id}
|
||||
size="md"
|
||||
disabled
|
||||
icon={<CopyIcon value={project.id} />}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="gradient"
|
||||
size="sm"
|
||||
className="mt-1"
|
||||
disabled={!updateProjectFormState.isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
<div className="mb-1">
|
||||
<Typography variant="h6">Transfer project</Typography>
|
||||
<Typography variant="small">
|
||||
Transfer this app to your personal account or a team you are a member
|
||||
of.
|
||||
<Link to="" className="text-blue-500">
|
||||
Learn more
|
||||
</Link>
|
||||
</Typography>
|
||||
<form
|
||||
onSubmit={handleTransfer(({ orgId }) => {
|
||||
setSelectedTransferOrganization(orgId);
|
||||
setOpenTransferDialog(!openTransferDialog);
|
||||
})}
|
||||
>
|
||||
<Typography variant="small" className="font-medium text-gray-800">
|
||||
Choose team
|
||||
</Typography>
|
||||
<Controller
|
||||
name="orgId"
|
||||
rules={{ required: 'This field is required' }}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<AsyncSelect
|
||||
{...field}
|
||||
// TODO: Implement placeholder for select
|
||||
label={!field.value ? 'Select an account / team' : ''}
|
||||
>
|
||||
{transferOrganizations.map((org, key) => (
|
||||
<Option key={key} value={org.id}>
|
||||
^ {org.name}
|
||||
</Option>
|
||||
))}
|
||||
</AsyncSelect>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
variant="gradient"
|
||||
size="sm"
|
||||
className="mt-1"
|
||||
disabled={!formState.isValid}
|
||||
type="submit"
|
||||
>
|
||||
Transfer
|
||||
</Button>
|
||||
</form>
|
||||
<ConfirmDialog
|
||||
dialogTitle="Transfer project"
|
||||
handleOpen={() => setOpenTransferDialog(!openTransferDialog)}
|
||||
open={openTransferDialog}
|
||||
confirmButtonTitle="Yes, Confirm transfer"
|
||||
handleConfirm={handleTransferProject}
|
||||
color="blue"
|
||||
>
|
||||
<Typography variant="small">
|
||||
Upon confirmation, your project {project.name} will be transferred
|
||||
from {project.organization.name} to {selectedUserOrgName}.
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
</div>
|
||||
<div className="mb-1">
|
||||
<Typography variant="h6">Delete project</Typography>
|
||||
<Typography variant="small">
|
||||
The project will be permanently deleted, including its deployments and
|
||||
domains. This action is irreversible and can not be undone.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="gradient"
|
||||
size="sm"
|
||||
color="red"
|
||||
onClick={handleDeleteProjectDialog}
|
||||
>
|
||||
^ Delete project
|
||||
</Button>
|
||||
<DeleteProjectDialog
|
||||
handleOpen={handleDeleteProjectDialog}
|
||||
open={openDeleteDialog}
|
||||
project={project}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralTabPanel;
|
||||
@@ -1,192 +0,0 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Project } from 'gql-client';
|
||||
|
||||
import { Button, Input, Switch, Typography } from '@material-tailwind/react';
|
||||
|
||||
import WebhookCard from './WebhookCard';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
|
||||
type UpdateProdBranchValues = {
|
||||
prodBranch: string;
|
||||
};
|
||||
|
||||
type UpdateWebhooksValues = {
|
||||
webhookUrl: string;
|
||||
};
|
||||
|
||||
const GitTabPanel = ({
|
||||
project,
|
||||
onUpdate,
|
||||
}: {
|
||||
project: Project;
|
||||
onUpdate: () => Promise<void>;
|
||||
}) => {
|
||||
const client = useGQLClient();
|
||||
|
||||
const {
|
||||
register: registerProdBranch,
|
||||
handleSubmit: handleSubmitProdBranch,
|
||||
reset: resetProdBranch,
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
prodBranch: project.prodBranch,
|
||||
},
|
||||
});
|
||||
|
||||
const updateProdBranchHandler: SubmitHandler<UpdateProdBranchValues> =
|
||||
useCallback(
|
||||
async (data) => {
|
||||
const { updateProject } = await client.updateProject(project.id, {
|
||||
prodBranch: data.prodBranch,
|
||||
});
|
||||
|
||||
if (updateProject) {
|
||||
await onUpdate();
|
||||
toast.success('Production branch upadated successfully');
|
||||
} else {
|
||||
toast.error('Error updating production branch');
|
||||
}
|
||||
},
|
||||
[project],
|
||||
);
|
||||
|
||||
const {
|
||||
register: registerWebhooks,
|
||||
handleSubmit: handleSubmitWebhooks,
|
||||
reset: resetWebhooks,
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
webhookUrl: '',
|
||||
},
|
||||
});
|
||||
|
||||
const updateWebhooksHandler: SubmitHandler<UpdateWebhooksValues> =
|
||||
useCallback(
|
||||
async (data) => {
|
||||
const { updateProject } = await client.updateProject(project.id, {
|
||||
webhooks: [...project.webhooks, data.webhookUrl],
|
||||
});
|
||||
|
||||
if (updateProject) {
|
||||
await onUpdate();
|
||||
toast.success('Webhook added successfully');
|
||||
} else {
|
||||
toast.error('Error adding webhook');
|
||||
}
|
||||
|
||||
resetWebhooks();
|
||||
},
|
||||
[project],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
resetProdBranch({
|
||||
prodBranch: project.prodBranch,
|
||||
});
|
||||
}, [project]);
|
||||
|
||||
const handleDeleteWebhook = async (index: number) => {
|
||||
project.webhooks.splice(index, 1);
|
||||
const { updateProject } = await client.updateProject(project.id, {
|
||||
webhooks: project.webhooks,
|
||||
});
|
||||
|
||||
if (updateProject) {
|
||||
await onUpdate();
|
||||
toast.success('Webhook deleted successfully');
|
||||
} else {
|
||||
toast.error('Error deleting webhook');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2 p-2">
|
||||
<Typography variant="h6" className="text-black">
|
||||
Git repository
|
||||
</Typography>
|
||||
|
||||
<div className="flex justify-between mt-4">
|
||||
<div>
|
||||
<Typography variant="small">Pull request comments</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Switch crossOrigin={undefined} defaultChecked />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<Typography variant="small">Commit comments</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Switch crossOrigin={undefined} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmitProdBranch(updateProdBranchHandler)}>
|
||||
<div className="mb-2 p-2">
|
||||
<Typography variant="h6" className="text-black">
|
||||
Production branch
|
||||
</Typography>
|
||||
<Typography variant="small">
|
||||
By default, each commit pushed to the{' '}
|
||||
<span className="font-bold">{project.prodBranch}</span> branch
|
||||
initiates a production deployment. You can opt for a different
|
||||
branch for deployment in the settings.
|
||||
</Typography>
|
||||
<Typography variant="small">Branch name</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
{...registerProdBranch('prodBranch')}
|
||||
/>
|
||||
<Button size="sm" className="mt-1" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form onSubmit={handleSubmitWebhooks(updateWebhooksHandler)}>
|
||||
<div className="mb-2 p-2">
|
||||
<Typography variant="h6" className="text-black">
|
||||
Deploy webhooks
|
||||
</Typography>
|
||||
<Typography variant="small">
|
||||
Webhooks configured to trigger when there is a change in a
|
||||
project's build or deployment status.
|
||||
</Typography>
|
||||
<div className="flex gap-1">
|
||||
<div className="grow">
|
||||
<Typography variant="small">Webhook URL</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
{...registerWebhooks('webhookUrl')}
|
||||
/>
|
||||
</div>
|
||||
<div className="self-end">
|
||||
<Button size="sm" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div className="mb-2 p-2">
|
||||
{project.webhooks.map((webhookUrl, index) => {
|
||||
return (
|
||||
<WebhookCard
|
||||
webhookUrl={webhookUrl}
|
||||
onDelete={() => handleDeleteWebhook(index)}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GitTabPanel;
|
||||
@@ -1,135 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import toast, { Toaster } from 'react-hot-toast';
|
||||
import {
|
||||
Permission,
|
||||
Project,
|
||||
AddProjectMemberInput,
|
||||
ProjectMember,
|
||||
} from 'gql-client';
|
||||
|
||||
import { Chip, Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
import MemberCard from './MemberCard';
|
||||
import AddMemberDialog from './AddMemberDialog';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
|
||||
const FIRST_MEMBER_CARD = 0;
|
||||
|
||||
const MembersTabPanel = ({ project }: { project: Project }) => {
|
||||
const client = useGQLClient();
|
||||
|
||||
const [addmemberDialogOpen, setAddMemberDialogOpen] = useState(false);
|
||||
|
||||
const [projectMembers, setProjectMembers] = useState<ProjectMember[]>([]);
|
||||
|
||||
const fetchProjectMembers = useCallback(async () => {
|
||||
const { projectMembers } = await client.getProjectMembers(project.id);
|
||||
setProjectMembers(projectMembers);
|
||||
}, [project.id]);
|
||||
|
||||
const addMemberHandler = useCallback(
|
||||
async (data: AddProjectMemberInput) => {
|
||||
const { addProjectMember: isProjectMemberAdded } =
|
||||
await client.addProjectMember(project.id, data);
|
||||
|
||||
if (isProjectMemberAdded) {
|
||||
await fetchProjectMembers();
|
||||
toast.success('Invitation sent');
|
||||
} else {
|
||||
toast.error('Invitation not sent');
|
||||
}
|
||||
},
|
||||
[project],
|
||||
);
|
||||
|
||||
const removeMemberHandler = async (projectMemberId: string) => {
|
||||
const { removeProjectMember: isMemberRemoved } =
|
||||
await client.removeProjectMember(projectMemberId);
|
||||
|
||||
if (isMemberRemoved) {
|
||||
await fetchProjectMembers();
|
||||
toast.success('Member removed from project');
|
||||
} else {
|
||||
toast.error('Not able to remove member');
|
||||
}
|
||||
};
|
||||
|
||||
const updateProjectMemberHandler = useCallback(
|
||||
async (projectMemberId: string, data: { permissions: Permission[] }) => {
|
||||
const { updateProjectMember: isProjectMemberUpdated } =
|
||||
await client.updateProjectMember(projectMemberId, data);
|
||||
|
||||
if (isProjectMemberUpdated) {
|
||||
await fetchProjectMembers();
|
||||
toast.success('Project member permission updated');
|
||||
} else {
|
||||
toast.error('Project member permission not updated');
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjectMembers();
|
||||
}, [project.id, fetchProjectMembers]);
|
||||
|
||||
return (
|
||||
<div className="p-2 mb-20">
|
||||
<div className="flex justify-between mb-2">
|
||||
<div className="flex">
|
||||
<Typography variant="h6">Members</Typography>
|
||||
<div>
|
||||
<Chip
|
||||
className="normal-case ml-3 font-normal"
|
||||
size="sm"
|
||||
value={projectMembers.length + 1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setAddMemberDialogOpen((preVal) => !preVal)}
|
||||
>
|
||||
+ Add member
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<MemberCard
|
||||
member={project.owner}
|
||||
isFirstCard={true}
|
||||
isOwner={true}
|
||||
isPending={false}
|
||||
permissions={[]}
|
||||
/>
|
||||
{projectMembers.map((projectMember, index) => {
|
||||
return (
|
||||
<MemberCard
|
||||
member={projectMember.member}
|
||||
key={projectMember.id}
|
||||
isFirstCard={index === FIRST_MEMBER_CARD}
|
||||
isOwner={projectMember.member.id === project.owner.id}
|
||||
isPending={projectMember.isPending}
|
||||
permissions={projectMember.permissions}
|
||||
onRemoveProjectMember={async () =>
|
||||
await removeMemberHandler(projectMember.id)
|
||||
}
|
||||
onUpdateProjectMember={async (data) => {
|
||||
await updateProjectMemberHandler(projectMember.id, data);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<AddMemberDialog
|
||||
handleOpen={() => {
|
||||
setAddMemberDialogOpen((preVal) => !preVal);
|
||||
}}
|
||||
open={addmemberDialogOpen}
|
||||
handleAddMember={addMemberHandler}
|
||||
/>
|
||||
<Toaster />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MembersTabPanel;
|
||||
Reference in New Issue
Block a user