diff --git a/ui/src/features/project/settings/components/delete-project-modal.tsx b/ui/src/features/project/settings/components/delete-project-modal.tsx new file mode 100644 index 000000000..98fe9d278 --- /dev/null +++ b/ui/src/features/project/settings/components/delete-project-modal.tsx @@ -0,0 +1,70 @@ +import { createConnectQueryKey, useMutation } from '@connectrpc/connect-query'; +import { useQueryClient } from '@tanstack/react-query'; +import { Alert, Form, Input, Modal } from 'antd'; +import React from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import yaml from 'yaml'; + +import { paths } from '@ui/config/paths'; +import { ModalComponentProps } from '@ui/features/common/modal/modal-context'; +import { + deleteResource, + listProjects +} from '@ui/gen/service/v1alpha1/service-KargoService_connectquery'; + +import { projectYAMLExample } from '../../list/utils/project-yaml-example'; + +export const DeleteProjectModal = ({ visible, hide }: ModalComponentProps) => { + const { name } = useParams(); + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const [inputValue, setInputValue] = React.useState(''); + + const { mutate, isPending } = useMutation(deleteResource, { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: createConnectQueryKey(listProjects) }); + navigate(paths.projects); + } + }); + + const onDelete = () => { + const textEncoder = new TextEncoder(); + const manifest = { + ...projectYAMLExample, + metadata: { + name + } + }; + + mutate({ manifest: textEncoder.encode(yaml.stringify(manifest)) }); + }; + + return ( + + +
+ + setInputValue(e.target.value)} + value={inputValue} + placeholder={name} + /> + +
+
+ ); +}; diff --git a/ui/src/features/project/settings/components/edit-project-modal.tsx b/ui/src/features/project/settings/components/edit-project-modal.tsx new file mode 100644 index 000000000..b2cde6125 --- /dev/null +++ b/ui/src/features/project/settings/components/edit-project-modal.tsx @@ -0,0 +1,77 @@ +import { useMutation, useQuery } from '@connectrpc/connect-query'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Modal } from 'antd'; +import type { JSONSchema4 } from 'json-schema'; +import { useForm } from 'react-hook-form'; +import { useParams } from 'react-router-dom'; +import yaml from 'yaml'; +import { z } from 'zod'; + +import { YamlEditor } from '@ui/features/common/code-editor/yaml-editor'; +import { FieldContainer } from '@ui/features/common/form/field-container'; +import { ModalComponentProps } from '@ui/features/common/modal/modal-context'; +import schema from '@ui/gen/schema/projects.kargo.akuity.io_v1alpha1.json'; +import { + getProject, + updateResource +} from '@ui/gen/service/v1alpha1/service-KargoService_connectquery'; +import { RawFormat } from '@ui/gen/service/v1alpha1/service_pb'; +import { decodeRawData } from '@ui/utils/decode-raw-data'; +import { zodValidators } from '@ui/utils/validators'; + +import { projectYAMLExample } from '../../list/utils/project-yaml-example'; + +const formSchema = z.object({ + value: zodValidators.requiredString +}); + +export const EditProjectModal = ({ visible, hide }: ModalComponentProps) => { + const { name } = useParams(); + const { data, isLoading } = useQuery(getProject, { name, format: RawFormat.YAML }); + + const { mutateAsync, isPending } = useMutation(updateResource, { + onSuccess: () => hide() + }); + + const { control, handleSubmit } = useForm({ + values: { + value: decodeRawData(data) + }, + resolver: zodResolver(formSchema) + }); + + const onSubmit = handleSubmit(async (data) => { + const textEncoder = new TextEncoder(); + await mutateAsync({ + manifest: textEncoder.encode(data.value) + }); + }); + + return ( + + + {({ field: { value, onChange } }) => ( + onChange(e || '')} + height='500px' + schema={schema as JSONSchema4} + placeholder={yaml.stringify(projectYAMLExample)} + isLoading={isLoading} + isHideManagedFieldsDisplayed + /> + )} + + + ); +}; diff --git a/ui/src/features/project/settings/project-settings.tsx b/ui/src/features/project/settings/project-settings.tsx new file mode 100644 index 000000000..c666a57bb --- /dev/null +++ b/ui/src/features/project/settings/project-settings.tsx @@ -0,0 +1,50 @@ +import { faChevronDown, faCog, faPencil, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Button, Dropdown, Space } from 'antd'; + +import { useModal } from '@ui/features/common/modal/use-modal'; + +import { DeleteProjectModal } from './components/delete-project-modal'; +import { EditProjectModal } from './components/edit-project-modal'; + +export const ProjectSettings = () => { + const { show: showEditModal } = useModal(EditProjectModal); + const { show: showDeleteModal } = useModal(DeleteProjectModal); + + return ( + + Edit + + ), + onClick: () => showEditModal() + }, + { + key: '2', + danger: true, + label: ( + <> + Delete + + ), + onClick: () => showDeleteModal() + } + ] + }} + placement='bottomRight' + trigger={['click']} + > + + + ); +}; diff --git a/ui/src/pages/project.tsx b/ui/src/pages/project.tsx index 66acb58ac..0c10a86a3 100644 --- a/ui/src/pages/project.tsx +++ b/ui/src/pages/project.tsx @@ -13,6 +13,7 @@ import { AnalysisTemplatesList } from '@ui/features/project/analysis-templates/a import { CredentialsList } from '@ui/features/project/credentials/credentials-list'; import { Events } from '@ui/features/project/events/events'; import { Pipelines } from '@ui/features/project/pipelines/pipelines'; +import { ProjectSettings } from '@ui/features/project/settings/project-settings'; const tabs = { pipelines: { @@ -67,6 +68,7 @@ export const Project = ({ tab = 'pipelines' }: { tab?: ProjectTab }) => {
PROJECT
{name}
+