diff --git a/packages/manager/.changeset/pr-12956-upcoming-features-1759484378173.md b/packages/manager/.changeset/pr-12956-upcoming-features-1759484378173.md
new file mode 100644
index 00000000000..02a0ce83878
--- /dev/null
+++ b/packages/manager/.changeset/pr-12956-upcoming-features-1759484378173.md
@@ -0,0 +1,5 @@
+---
+"@linode/manager": Upcoming Features
+---
+
+Add dialog modal for Delete action for Logs Stream and Destination ([#12956](https://github.com/linode/manager/pull/12956))
diff --git a/packages/manager/src/features/Delivery/Destinations/DeleteDestinationDialog.tsx b/packages/manager/src/features/Delivery/Destinations/DeleteDestinationDialog.tsx
new file mode 100644
index 00000000000..020f86a47e8
--- /dev/null
+++ b/packages/manager/src/features/Delivery/Destinations/DeleteDestinationDialog.tsx
@@ -0,0 +1,61 @@
+import { useDeleteDestinationMutation } from '@linode/queries';
+import { ActionsPanel } from '@linode/ui';
+import { enqueueSnackbar } from 'notistack';
+import * as React from 'react';
+
+import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog';
+
+import type { Destination } from '@linode/api-v4';
+
+interface Props {
+ destination: Destination | undefined;
+ onClose: () => void;
+ open: boolean;
+}
+
+export const DeleteDestinationDialog = React.memo((props: Props) => {
+ const { onClose, open, destination } = props;
+ const {
+ mutateAsync: deleteDestination,
+ isPending,
+ error,
+ } = useDeleteDestinationMutation();
+
+ const handleDelete = () => {
+ const { id, label } = destination as Destination;
+ deleteDestination({
+ id,
+ }).then(() => {
+ onClose();
+ return enqueueSnackbar(`Destination ${label} deleted successfully`, {
+ variant: 'success',
+ });
+ });
+ };
+
+ const actions = (
+
+ );
+
+ return (
+
+ Are you sure you want to delete "{destination?.label}"
+ destination?
+
+ );
+});
diff --git a/packages/manager/src/features/Delivery/Destinations/DestinationTableRow.tsx b/packages/manager/src/features/Delivery/Destinations/DestinationTableRow.tsx
index 8f68e259054..0fed3b16756 100644
--- a/packages/manager/src/features/Delivery/Destinations/DestinationTableRow.tsx
+++ b/packages/manager/src/features/Delivery/Destinations/DestinationTableRow.tsx
@@ -8,8 +8,8 @@ import { TableRow } from 'src/components/TableRow';
import { getDestinationTypeOption } from 'src/features/Delivery/deliveryUtils';
import { DestinationActionMenu } from 'src/features/Delivery/Destinations/DestinationActionMenu';
-import type { DestinationHandlers } from './DestinationActionMenu';
import type { Destination } from '@linode/api-v4';
+import type { DestinationHandlers } from 'src/features/Delivery/Destinations/DestinationActionMenu';
interface DestinationTableRowProps extends DestinationHandlers {
destination: Destination;
diff --git a/packages/manager/src/features/Delivery/Destinations/DestinationsLanding.test.tsx b/packages/manager/src/features/Delivery/Destinations/DestinationsLanding.test.tsx
index da0c92e193d..767f1acb704 100644
--- a/packages/manager/src/features/Delivery/Destinations/DestinationsLanding.test.tsx
+++ b/packages/manager/src/features/Delivery/Destinations/DestinationsLanding.test.tsx
@@ -1,4 +1,4 @@
-import { screen } from '@testing-library/react';
+import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { beforeEach, describe, expect } from 'vitest';
@@ -136,6 +136,12 @@ describe('Destinations Landing Table', () => {
await userEvent.click(screen.getByText(itemText));
};
+ const checkClosedModal = async (modal: HTMLElement) => {
+ await waitFor(() => {
+ expect(modal).not.toBeInTheDocument();
+ });
+ };
+
describe('given action menu', () => {
beforeEach(() => {
queryMocks.useDestinationsQuery.mockReturnValue({
@@ -173,9 +179,30 @@ describe('Destinations Landing Table', () => {
await clickOnActionMenu();
await clickOnActionMenuItem('Delete');
+ const deleteDestinationModal = screen.getByText('Delete Destination');
+ expect(deleteDestinationModal).toBeInTheDocument();
+
+ // get modal Cancel button
+ const cancelModalDialogButton = screen.getByRole('button', {
+ name: 'Cancel',
+ });
+ await userEvent.click(cancelModalDialogButton);
+ await checkClosedModal(deleteDestinationModal);
+
+ await clickOnActionMenu();
+ await clickOnActionMenuItem('Delete');
+
+ // get delete Destination button
+ const deleteDestinationButton = screen.getByRole('button', {
+ name: 'Delete',
+ });
+ await userEvent.click(deleteDestinationButton);
+
expect(mockDeleteDestinationMutation).toHaveBeenCalledWith({
id: 1,
});
+
+ await checkClosedModal(deleteDestinationModal);
});
});
});
diff --git a/packages/manager/src/features/Delivery/Destinations/DestinationsLanding.tsx b/packages/manager/src/features/Delivery/Destinations/DestinationsLanding.tsx
index e7e0819165c..81e7750f376 100644
--- a/packages/manager/src/features/Delivery/Destinations/DestinationsLanding.tsx
+++ b/packages/manager/src/features/Delivery/Destinations/DestinationsLanding.tsx
@@ -1,12 +1,8 @@
-import {
- useDeleteDestinationMutation,
- useDestinationsQuery,
-} from '@linode/queries';
+import { useDestinationsQuery } from '@linode/queries';
import { CircleProgress, ErrorState, Hidden } from '@linode/ui';
import { TableBody, TableHead, TableRow } from '@mui/material';
import Table from '@mui/material/Table';
import { useNavigate, useSearch } from '@tanstack/react-router';
-import { enqueueSnackbar } from 'notistack';
import * as React from 'react';
import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter';
@@ -18,20 +14,24 @@ import {
DESTINATIONS_TABLE_DEFAULT_ORDER_BY,
DESTINATIONS_TABLE_PREFERENCE_KEY,
} from 'src/features/Delivery/Destinations/constants';
+import { DeleteDestinationDialog } from 'src/features/Delivery/Destinations/DeleteDestinationDialog';
import { DestinationsLandingEmptyState } from 'src/features/Delivery/Destinations/DestinationsLandingEmptyState';
import { DestinationTableRow } from 'src/features/Delivery/Destinations/DestinationTableRow';
import { DeliveryTabHeader } from 'src/features/Delivery/Shared/DeliveryTabHeader/DeliveryTabHeader';
import { useOrderV2 } from 'src/hooks/useOrderV2';
import { usePaginationV2 } from 'src/hooks/usePaginationV2';
-import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';
import type { Destination } from '@linode/api-v4';
import type { DestinationHandlers } from 'src/features/Delivery/Destinations/DestinationActionMenu';
export const DestinationsLanding = () => {
const navigate = useNavigate();
- const { mutateAsync: deleteDestination } = useDeleteDestinationMutation();
const destinationsUrl = '/logs/delivery/destinations';
+ const [deleteDialogOpen, setDeleteDialogOpen] =
+ React.useState(false);
+ const [deleteDestinationSelection, setDeleteDestinationSelection] =
+ React.useState();
+
const search = useSearch({
from: destinationsUrl,
shouldThrow: false,
@@ -104,31 +104,18 @@ export const DestinationsLanding = () => {
navigate({ to: `/logs/delivery/destinations/${id}/edit` });
};
- const handleDelete = ({ id, label }: Destination) => {
- deleteDestination({
- id,
- })
- .then(() => {
- return enqueueSnackbar(`Destination ${label} deleted successfully`, {
- variant: 'success',
- });
- })
- .catch((error) => {
- return enqueueSnackbar(
- getAPIErrorOrDefault(
- error,
- `There was an issue deleting your destination`
- )[0].reason,
- {
- variant: 'error',
- }
- );
- });
+ const openDeleteDialog = (destination: Destination) => {
+ setDeleteDestinationSelection(destination);
+ setDeleteDialogOpen(true);
+ };
+
+ const closeDeleteDialog = () => {
+ setDeleteDialogOpen(false);
};
const handlers: DestinationHandlers = {
onEdit: handleEdit,
- onDelete: handleDelete,
+ onDelete: openDeleteDialog,
};
return (
@@ -213,6 +200,11 @@ export const DestinationsLanding = () => {
page={pagination.page}
pageSize={pagination.pageSize}
/>
+
>
)}
>
diff --git a/packages/manager/src/features/Delivery/Streams/DeleteStreamDialog.tsx b/packages/manager/src/features/Delivery/Streams/DeleteStreamDialog.tsx
new file mode 100644
index 00000000000..b2344bb573b
--- /dev/null
+++ b/packages/manager/src/features/Delivery/Streams/DeleteStreamDialog.tsx
@@ -0,0 +1,60 @@
+import { useDeleteStreamMutation } from '@linode/queries';
+import { ActionsPanel } from '@linode/ui';
+import { enqueueSnackbar } from 'notistack';
+import * as React from 'react';
+
+import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog';
+
+import type { Stream } from '@linode/api-v4';
+
+interface Props {
+ onClose: () => void;
+ open: boolean;
+ stream: Stream | undefined;
+}
+
+export const DeleteStreamDialog = React.memo((props: Props) => {
+ const { onClose, open, stream } = props;
+ const {
+ mutateAsync: deleteStream,
+ isPending,
+ error,
+ } = useDeleteStreamMutation();
+
+ const handleDelete = () => {
+ const { id, label } = stream as Stream;
+ deleteStream({
+ id,
+ }).then(() => {
+ onClose();
+ return enqueueSnackbar(`Stream ${label} deleted successfully`, {
+ variant: 'success',
+ });
+ });
+ };
+
+ const actions = (
+
+ );
+
+ return (
+
+ Are you sure you want to delete "{stream?.label}" stream?
+
+ );
+});
diff --git a/packages/manager/src/features/Delivery/Streams/StreamActionMenu.tsx b/packages/manager/src/features/Delivery/Streams/StreamActionMenu.tsx
index a5d061c54cf..81c0e8a3eeb 100644
--- a/packages/manager/src/features/Delivery/Streams/StreamActionMenu.tsx
+++ b/packages/manager/src/features/Delivery/Streams/StreamActionMenu.tsx
@@ -3,13 +3,13 @@ import * as React from 'react';
import { ActionMenu } from 'src/components/ActionMenu/ActionMenu';
-export interface Handlers {
+export interface StreamHandlers {
onDelete: (stream: Stream) => void;
onDisableOrEnable: (stream: Stream) => void;
onEdit: (stream: Stream) => void;
}
-interface StreamActionMenuProps extends Handlers {
+interface StreamActionMenuProps extends StreamHandlers {
stream: Stream;
}
diff --git a/packages/manager/src/features/Delivery/Streams/StreamTableRow.tsx b/packages/manager/src/features/Delivery/Streams/StreamTableRow.tsx
index 13cf2ab559f..ec1c2c80b19 100644
--- a/packages/manager/src/features/Delivery/Streams/StreamTableRow.tsx
+++ b/packages/manager/src/features/Delivery/Streams/StreamTableRow.tsx
@@ -12,8 +12,8 @@ import {
} from 'src/features/Delivery/deliveryUtils';
import { StreamActionMenu } from 'src/features/Delivery/Streams/StreamActionMenu';
-import type { Handlers as StreamHandlers } from './StreamActionMenu';
import type { Stream, StreamStatus } from '@linode/api-v4';
+import type { StreamHandlers } from 'src/features/Delivery/Streams/StreamActionMenu';
interface StreamTableRowProps extends StreamHandlers {
stream: Stream;
diff --git a/packages/manager/src/features/Delivery/Streams/StreamsLanding.test.tsx b/packages/manager/src/features/Delivery/Streams/StreamsLanding.test.tsx
index dbf71d20663..b9e21d51fd7 100644
--- a/packages/manager/src/features/Delivery/Streams/StreamsLanding.test.tsx
+++ b/packages/manager/src/features/Delivery/Streams/StreamsLanding.test.tsx
@@ -1,4 +1,4 @@
-import { screen, within } from '@testing-library/react';
+import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { beforeEach, describe, expect } from 'vitest';
@@ -144,6 +144,12 @@ describe('Streams Landing Table', () => {
await userEvent.click(screen.getByText(itemText));
};
+ const checkClosedModal = async (modal: HTMLElement) => {
+ await waitFor(() => {
+ expect(modal).not.toBeInTheDocument();
+ });
+ };
+
describe('given action menu', () => {
beforeEach(() => {
queryMocks.useStreamsQuery.mockReturnValue({
@@ -224,9 +230,30 @@ describe('Streams Landing Table', () => {
await clickOnActionMenu();
await clickOnActionMenuItem('Delete');
+ const deleteStreamModal = screen.getByText('Delete Stream');
+ expect(deleteStreamModal).toBeInTheDocument();
+
+ // get modal Cancel button
+ const cancelModalDialogButton = screen.getByRole('button', {
+ name: 'Cancel',
+ });
+ await userEvent.click(cancelModalDialogButton);
+ await checkClosedModal(deleteStreamModal);
+
+ await clickOnActionMenu();
+ await clickOnActionMenuItem('Delete');
+
+ // get delete Stream button
+ const deleteStreamButton = screen.getByRole('button', {
+ name: 'Delete',
+ });
+ await userEvent.click(deleteStreamButton);
+
expect(mockDeleteStreamMutation).toHaveBeenCalledWith({
id: 1,
});
+
+ await checkClosedModal(deleteStreamModal);
});
});
});
diff --git a/packages/manager/src/features/Delivery/Streams/StreamsLanding.tsx b/packages/manager/src/features/Delivery/Streams/StreamsLanding.tsx
index 1e27f0ccc67..ba460e72726 100644
--- a/packages/manager/src/features/Delivery/Streams/StreamsLanding.tsx
+++ b/packages/manager/src/features/Delivery/Streams/StreamsLanding.tsx
@@ -1,9 +1,5 @@
import { streamStatus } from '@linode/api-v4';
-import {
- useDeleteStreamMutation,
- useStreamsQuery,
- useUpdateStreamMutation,
-} from '@linode/queries';
+import { useStreamsQuery, useUpdateStreamMutation } from '@linode/queries';
import { CircleProgress, ErrorState, Hidden } from '@linode/ui';
import { TableBody, TableCell, TableHead, TableRow } from '@mui/material';
import Table from '@mui/material/Table';
@@ -21,18 +17,26 @@ import {
STREAMS_TABLE_DEFAULT_ORDER_BY,
STREAMS_TABLE_PREFERENCE_KEY,
} from 'src/features/Delivery/Streams/constants';
+import { DeleteStreamDialog } from 'src/features/Delivery/Streams/DeleteStreamDialog';
import { StreamsLandingEmptyState } from 'src/features/Delivery/Streams/StreamsLandingEmptyState';
import { StreamTableRow } from 'src/features/Delivery/Streams/StreamTableRow';
import { useOrderV2 } from 'src/hooks/useOrderV2';
import { usePaginationV2 } from 'src/hooks/usePaginationV2';
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';
-import type { Handlers as StreamHandlers } from './StreamActionMenu';
+import type { StreamHandlers } from './StreamActionMenu';
import type { Stream } from '@linode/api-v4';
export const StreamsLanding = () => {
const navigate = useNavigate();
const streamsUrl = '/logs/delivery/streams';
+
+ const [deleteDialogOpen, setDeleteDialogOpen] =
+ React.useState(false);
+ const [deleteStreamSelection, setDeleteStreamSelection] = React.useState<
+ Stream | undefined
+ >();
+
const search = useSearch({
from: '/logs/delivery/streams',
shouldThrow: false,
@@ -54,7 +58,6 @@ export const StreamsLanding = () => {
});
const { mutateAsync: updateStream } = useUpdateStreamMutation();
- const { mutateAsync: deleteStream } = useDeleteStreamMutation();
const filter = {
['+order']: order,
@@ -120,26 +123,13 @@ export const StreamsLanding = () => {
navigate({ to: `/logs/delivery/streams/${id}/edit` });
};
- const handleDelete = ({ id, label }: Stream) => {
- deleteStream({
- id,
- })
- .then(() => {
- return enqueueSnackbar(`Stream ${label} deleted successfully`, {
- variant: 'success',
- });
- })
- .catch((error) => {
- return enqueueSnackbar(
- getAPIErrorOrDefault(
- error,
- `There was an issue deleting your stream`
- )[0].reason,
- {
- variant: 'error',
- }
- );
- });
+ const openDeleteDialog = (stream: Stream) => {
+ setDeleteStreamSelection(stream);
+ setDeleteDialogOpen(true);
+ };
+
+ const closeDeleteDialog = () => {
+ setDeleteDialogOpen(false);
};
const handleDisableOrEnable = ({
@@ -183,7 +173,7 @@ export const StreamsLanding = () => {
const handlers: StreamHandlers = {
onDisableOrEnable: handleDisableOrEnable,
onEdit: handleEdit,
- onDelete: handleDelete,
+ onDelete: openDeleteDialog,
};
return (
@@ -264,6 +254,11 @@ export const StreamsLanding = () => {
page={pagination.page}
pageSize={pagination.pageSize}
/>
+
>
)}
>