Skip to content

Commit

Permalink
Provide ways to delete mirror and credential
Browse files Browse the repository at this point in the history
  • Loading branch information
minwoox committed Nov 1, 2024
1 parent 92a8546 commit 3e6ba33
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ void cruTest() {
createAndReadMirror();
updateMirror();
rejectInvalidRepositoryUri();
deleteMirror();
}

private void rejectInvalidRepositoryUri() {
Expand Down Expand Up @@ -312,6 +313,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,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.credentialFile;

import java.util.List;
import java.util.concurrent.CompletableFuture;
Expand All @@ -31,8 +32,13 @@
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 @@ -127,16 +133,17 @@ public CompletableFuture<PushResultDto> updateCredential(@Param String projectNa
*/
@RequiresWritePermission(repository = Project.REPO_META)
@Delete("/projects/{projectName}/credentials/{id}")
public CompletableFuture<PushResultDto> deleteCredential(@Param String projectName,
@Param String id, Author author) {
public CompletableFuture<Void> deleteCredential(@Param String projectName,
@Param String id, Author author) {
final MetaRepository metaRepository = metaRepo(projectName);
metaRepository.credential(id).thenApply(credential -> {
return metaRepository.credential(id).thenCompose(credential -> {
// credential exists.
metaRepository.removeCredential();
return null;
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);
});
checkArgument(id.equals(credential.id()), "The credential ID (%s) can't be updated", id);
return createOrUpdate(projectName, credential, author, true);
}

private CompletableFuture<PushResultDto> createOrUpdate(String projectName,
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 @@ -123,6 +130,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
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
@@ -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, 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,67 @@
import {
Button,
HStack,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
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';

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" variant="ghost" onClick={onToggle}>
Delete
</Button>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Are you sure?</ModalHeader>
<ModalCloseButton />
<ModalBody>
Delete credential '{id}' 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
@@ -0,0 +1,67 @@
import {
Button,
HStack,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
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';

export const DeleteMirror = ({
projectName,
id,
deleteMirror,
isLoading,
}: {
projectName: string;
id: string;
deleteMirror: (projectName: string, id: string, ) => Promise<void>;
isLoading: boolean;
}): JSX.Element => {
const { isOpen, onToggle, onClose } = useDisclosure();
const dispatch = useAppDispatch();
const handleDelete = async () => {
try {
await deleteMirror(projectName, id);
dispatch(newNotification('Mirror 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" variant="ghost" onClick={onToggle}>
Delete
</Button>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Are you sure?</ModalHeader>
<ModalCloseButton />
<ModalBody>
Delete mirror '{id}' 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>
</>
);
};
Loading

0 comments on commit 3e6ba33

Please sign in to comment.