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} /> + )}