Skip to content

Commit

Permalink
Provide ways to delete mirror and credential (line#1051)
Browse files Browse the repository at this point in the history
There's no way to delete mirror and credential

Result:
- You can now delete mirror and credential.
  • Loading branch information
minwoox authored Nov 13, 2024
1 parent dee2f40 commit 302dcda
Show file tree
Hide file tree
Showing 14 changed files with 374 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ void cruTest() {
createAndReadMirror();
updateMirror();
rejectInvalidRepositoryUri();
deleteMirror();
deleteCredential();
}

private void rejectInvalidRepositoryUri() {
Expand Down Expand Up @@ -325,6 +327,42 @@ private void updateMirror() {
assertThat(savedMirror).isEqualTo(mirror);
}

private void deleteMirror() {
final String mirrorId = "mirror-2";
assertThat(userClient.prepare()
.delete("/api/v1/projects/{proj}/mirrors/{id}")
.pathParam("proj", FOO_PROJ)
.pathParam("id", mirrorId)
.execute()
.status())
.isEqualTo(HttpStatus.NO_CONTENT);
assertThat(userClient.prepare()
.get("/api/v1/projects/{proj}/mirrors/{id}")
.pathParam("proj", FOO_PROJ)
.pathParam("id", mirrorId)
.execute()
.status())
.isEqualTo(HttpStatus.NOT_FOUND);
}

private void deleteCredential() {
final String credentialId = "public-key-credential";
assertThat(userClient.prepare()
.delete("/api/v1/projects/{proj}/credentials/{id}")
.pathParam("proj", FOO_PROJ)
.pathParam("id", credentialId)
.execute()
.status())
.isEqualTo(HttpStatus.NO_CONTENT);
assertThat(userClient.prepare()
.get("/api/v1/projects/{proj}/credentials/{id}")
.pathParam("proj", FOO_PROJ)
.pathParam("id", credentialId)
.execute()
.status())
.isEqualTo(HttpStatus.NOT_FOUND);
}

private static MirrorDto newMirror(String id) {
return new MirrorDto(id,
true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,27 @@

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.linecorp.centraldogma.server.internal.storage.repository.DefaultMetaRepository.credentialFile;

import java.util.List;
import java.util.concurrent.CompletableFuture;

import com.linecorp.armeria.server.annotation.ConsumesJson;
import com.linecorp.armeria.server.annotation.Delete;
import com.linecorp.armeria.server.annotation.Get;
import com.linecorp.armeria.server.annotation.Param;
import com.linecorp.armeria.server.annotation.Post;
import com.linecorp.armeria.server.annotation.ProducesJson;
import com.linecorp.armeria.server.annotation.Put;
import com.linecorp.armeria.server.annotation.StatusCode;
import com.linecorp.centraldogma.common.Author;
import com.linecorp.centraldogma.common.Change;
import com.linecorp.centraldogma.common.Markup;
import com.linecorp.centraldogma.common.Revision;
import com.linecorp.centraldogma.internal.api.v1.PushResultDto;
import com.linecorp.centraldogma.server.command.Command;
import com.linecorp.centraldogma.server.command.CommandExecutor;
import com.linecorp.centraldogma.server.command.CommitResult;
import com.linecorp.centraldogma.server.credential.Credential;
import com.linecorp.centraldogma.server.internal.api.auth.RequiresReadPermission;
import com.linecorp.centraldogma.server.internal.api.auth.RequiresWritePermission;
Expand Down Expand Up @@ -118,6 +125,26 @@ public CompletableFuture<PushResultDto> updateCredential(@Param String projectNa
return createOrUpdate(projectName, credential, author, user, true);
}

/**
* DELETE /projects/{projectName}/credentials/{id}
*
* <p>Delete the existing credential.
*/
@RequiresWritePermission(repository = Project.REPO_META)
@Delete("/projects/{projectName}/credentials/{id}")
public CompletableFuture<Void> deleteCredential(@Param String projectName,
@Param String id, Author author, User user) {
final MetaRepository metaRepository = metaRepo(projectName, user);
return metaRepository.credential(id).thenCompose(credential -> {
// credential exists.
final Command<CommitResult> command =
Command.push(author, projectName, metaRepository.name(),
Revision.HEAD, "Delete credential: " + id, "",
Markup.PLAINTEXT, Change.ofRemoval(credentialFile(id)));
return executor().execute(command).thenApply(result -> null);
});
}

private CompletableFuture<PushResultDto> createOrUpdate(String projectName, Credential credential,
Author author, User user, boolean update) {
return metaRepo(projectName, user).createPushCommand(credential, author, update).thenCompose(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.linecorp.centraldogma.server.internal.storage.repository.DefaultMetaRepository.mirrorFile;

import java.net.URI;
import java.util.List;
Expand All @@ -27,6 +28,7 @@
import com.cronutils.model.Cron;

import com.linecorp.armeria.server.annotation.ConsumesJson;
import com.linecorp.armeria.server.annotation.Delete;
import com.linecorp.armeria.server.annotation.Get;
import com.linecorp.armeria.server.annotation.Param;
import com.linecorp.armeria.server.annotation.Post;
Expand All @@ -35,9 +37,14 @@
import com.linecorp.armeria.server.annotation.StatusCode;
import com.linecorp.armeria.server.annotation.decorator.RequestTimeout;
import com.linecorp.centraldogma.common.Author;
import com.linecorp.centraldogma.common.Change;
import com.linecorp.centraldogma.common.Markup;
import com.linecorp.centraldogma.common.Revision;
import com.linecorp.centraldogma.internal.api.v1.MirrorDto;
import com.linecorp.centraldogma.internal.api.v1.PushResultDto;
import com.linecorp.centraldogma.server.command.Command;
import com.linecorp.centraldogma.server.command.CommandExecutor;
import com.linecorp.centraldogma.server.command.CommitResult;
import com.linecorp.centraldogma.server.internal.api.auth.RequiresReadPermission;
import com.linecorp.centraldogma.server.internal.api.auth.RequiresWritePermission;
import com.linecorp.centraldogma.server.internal.mirror.MirrorRunner;
Expand Down Expand Up @@ -124,6 +131,26 @@ public CompletableFuture<PushResultDto> updateMirror(@Param String projectName,
return createOrUpdate(projectName, mirror, author, true);
}

/**
* DELETE /projects/{projectName}/mirrors/{id}
*
* <p>Delete the existing mirror.
*/
@RequiresWritePermission(repository = Project.REPO_META)
@Delete("/projects/{projectName}/mirrors/{id}")
public CompletableFuture<Void> deleteMirror(@Param String projectName,
@Param String id, Author author) {
final MetaRepository metaRepository = metaRepo(projectName);
return metaRepository.mirror(id).thenCompose(mirror -> {
// mirror exists.
final Command<CommitResult> command =
Command.push(author, projectName, metaRepository.name(),
Revision.HEAD, "Delete mirror: " + id, "",
Markup.PLAINTEXT, Change.ofRemoval(mirrorFile(id)));
return executor().execute(command).thenApply(result -> null);
});
}

private CompletableFuture<PushResultDto> createOrUpdate(String projectName,
MirrorDto newMirror,
Author author, boolean update) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public CompletableFuture<Mirror> mirror(String id) {
@SuppressWarnings("unchecked")
final Entry<JsonNode> entry = (Entry<JsonNode>) entries.get(mirrorFile);
if (entry == null) {
throw new EntryNotFoundException("failed to find credential '" + mirrorFile + "' in " +
throw new EntryNotFoundException("failed to find mirror '" + mirrorFile + "' in " +
parent().name() + '/' + name());
}

Expand Down
16 changes: 16 additions & 0 deletions webapp/src/dogma/features/api/apiSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,13 @@ export const apiSlice = createApi({
}),
invalidatesTags: ['Metadata'],
}),
deleteMirror: builder.mutation({
query: ({ projectName, id }) => ({
url: `/api/v1/projects/${projectName}/mirrors/${id}`,
method: 'DELETE',
}),
invalidatesTags: ['Metadata'],
}),
runMirror: builder.mutation<MirrorResult, { projectName: string; id: string }>({
query: ({ projectName, id }) => ({
url: `/api/v1/projects/${projectName}/mirrors/${id}/run`,
Expand Down Expand Up @@ -380,6 +387,13 @@ export const apiSlice = createApi({
}),
invalidatesTags: ['Metadata'],
}),
deleteCredential: builder.mutation({
query: ({ projectName, id }) => ({
url: `/api/v1/projects/${projectName}/credentials/${id}`,
method: 'DELETE',
}),
invalidatesTags: ['Metadata'],
}),
getTitle: builder.query<TitleDto, void>({
query: () => ({
baseUrl: '',
Expand Down Expand Up @@ -430,12 +444,14 @@ export const {
useGetMirrorQuery,
useAddNewMirrorMutation,
useUpdateMirrorMutation,
useDeleteMirrorMutation,
useRunMirrorMutation,
// Credential
useGetCredentialsQuery,
useGetCredentialQuery,
useAddNewCredentialMutation,
useUpdateCredentialMutation,
useDeleteCredentialMutation,
// Title
useGetTitleQuery,
} = apiSlice;
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
Button,
HStack,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
} from '@chakra-ui/react';

interface DeleteConfirmationModalProps {
isOpen: boolean;
onClose: () => void;
type: string;
id: string;
projectName: string;
handleDelete: () => void;
isLoading: boolean;
}

export const DeleteConfirmationModal = ({
isOpen,
onClose,
id,
type,
projectName,
handleDelete,
isLoading,
}: DeleteConfirmationModalProps): JSX.Element => {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Are you sure?</ModalHeader>
<ModalCloseButton />
<ModalBody>
Delete {type} &apos;{id}&apos; from {projectName}?
</ModalBody>
<ModalFooter>
<HStack spacing={3}>
<Button colorScheme="red" variant="outline" onClick={onClose}>
Cancel
</Button>
<Button colorScheme="red" onClick={handleDelete} isLoading={isLoading} loadingText="Deleting">
Delete
</Button>
</HStack>
</ModalFooter>
</ModalContent>
</Modal>
);
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ColumnDef, createColumnHelper } from '@tanstack/react-table';
import React, { useMemo } from 'react';
import { DataTableClientPagination } from 'dogma/common/components/table/DataTableClientPagination';
import { useGetCredentialsQuery } from 'dogma/features/api/apiSlice';
import { useGetCredentialsQuery, useDeleteCredentialMutation } from 'dogma/features/api/apiSlice';
import { Badge } from '@chakra-ui/react';
import { ChakraLink } from 'dogma/common/components/ChakraLink';
import { CredentialDto } from 'dogma/features/project/settings/credentials/CredentialDto';
import { DeleteCredential } from 'dogma/features/project/settings/credentials/DeleteCredential';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export type CredentialListProps<Data extends object> = {
Expand All @@ -13,6 +14,7 @@ export type CredentialListProps<Data extends object> = {

const CredentialList = <Data extends object>({ projectName }: CredentialListProps<Data>) => {
const { data } = useGetCredentialsQuery(projectName);
const [deleteCredential, { isLoading }] = useDeleteCredentialMutation();
const columnHelper = createColumnHelper<CredentialDto>();
const columns = useMemo(
() => [
Expand Down Expand Up @@ -46,8 +48,20 @@ const CredentialList = <Data extends object>({ projectName }: CredentialListProp
},
header: 'Status',
}),
columnHelper.accessor((row: CredentialDto) => row.id, {
cell: (info) => (
<DeleteCredential
projectName={projectName}
id={info.getValue()}
deleteCredential={(projectName, id) => deleteCredential({ projectName, id }).unwrap()}
isLoading={isLoading}
/>
),
header: 'Actions',
enableSorting: false,
}),
],
[columnHelper, projectName],
[columnHelper, deleteCredential, isLoading, projectName],
);
return <DataTableClientPagination columns={columns as ColumnDef<CredentialDto>[]} data={data || []} />;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Button, useDisclosure } from '@chakra-ui/react';
import { newNotification } from 'dogma/features/notification/notificationSlice';
import ErrorMessageParser from 'dogma/features/services/ErrorMessageParser';
import { useAppDispatch } from 'dogma/hooks';
import { MdDelete } from 'react-icons/md';
import { DeleteConfirmationModal } from 'dogma/features/project/settings/DeleteConfirmationModal';

export const DeleteCredential = ({
projectName,
id,
deleteCredential,
isLoading,
}: {
projectName: string;
id: string;
deleteCredential: (projectName: string, id: string) => Promise<void>;
isLoading: boolean;
}): JSX.Element => {
const { isOpen, onToggle, onClose } = useDisclosure();
const dispatch = useAppDispatch();
const handleDelete = async () => {
try {
await deleteCredential(projectName, id);
dispatch(newNotification('Credential deleted.', `Successfully deleted ${id}`, 'success'));
onClose();
} catch (error) {
dispatch(newNotification(`Failed to delete ${id}`, ErrorMessageParser.parse(error), 'error'));
}
};
return (
<>
<Button leftIcon={<MdDelete />} colorScheme="red" size="sm" onClick={onToggle}>
Delete
</Button>
<DeleteConfirmationModal
isOpen={isOpen}
onClose={onClose}
id={id}
type={'credential'}
projectName={projectName}
handleDelete={handleDelete}
isLoading={isLoading}
/>
</>
);
};
Loading

0 comments on commit 302dcda

Please sign in to comment.