From 9600eb6fa12424f1151a6167514db715809302d4 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 8 Apr 2024 17:21:41 +0300 Subject: [PATCH] refactor(tables): use add and delete buttons [EE-6297] (#10668) Co-authored-by: Chaim Lev-Ari --- app/docker/views/configs/configsController.js | 5 -- .../views/networks/networksController.js | 5 -- app/docker/views/secrets/secretsController.js | 5 -- app/docker/views/volumes/volumesController.js | 40 ++++----- .../applications/applicationsController.js | 6 +- app/portainer/react/views/wizard.ts | 2 +- .../views/stacks/stacksController.js | 9 +- .../ListView/ContainersDatatable.tsx | 35 +++----- app/react/components/Link.tsx | 18 ++-- app/react/components/buttons/DeleteButton.tsx | 13 ++- .../ConfigsDatatable/ConfigsDatatable.tsx | 27 ++---- .../ContainersDatatableActions.tsx | 23 ++--- .../ImagesDatatable/ImagesDatatable.tsx | 19 ++--- .../docker/networks/ItemView/ItemView.tsx | 22 ++--- .../networks/ItemView/NetworkDetailsTable.tsx | 32 ++++--- .../networks/ListView/NetworksDatatable.tsx | 26 +++--- .../secrets/ListView/SecretsDatatable.tsx | 28 ++----- .../ServicesDatatable/TableActions.tsx | 35 ++------ .../ListView/StacksDatatable/TableActions.tsx | 28 ++----- .../VolumesDatatable/TableActions.tsx | 28 ++----- .../Datatable/TableActions.tsx | 28 ++----- .../EdgeStacksDatatable/TableActions.tsx | 40 ++------- .../ApplicationDetailsWidget.tsx | 20 ++--- .../TableActions.tsx | 15 ++-- .../ConfigMapsDatatable.tsx | 63 +++++--------- .../SecretsDatatable/SecretsDatatable.tsx | 61 +++++--------- .../IngressDatatable/IngressDatatable.tsx | 59 ++++--------- app/react/kubernetes/ingresses/style.css | 1 - .../ServicesDatatable/ServicesDatatable.tsx | 83 +++++++------------ .../HelmRepositoryDatatableActions.tsx | 43 ++++------ .../ListView/EnvironmentsDatatable.tsx | 31 +++---- .../ListView/ImportFdoDeviceButton.tsx | 20 ++--- .../update-schedules/ListView/ListView.tsx | 30 ++----- .../EnvironmentsCreationView.tsx | 5 +- .../notifications/NotificationsView.tsx | 15 ++-- .../FDOProfilesDatatableActions.tsx | 33 +++----- .../AppTemplatesListItem.test.tsx | 65 +++++++++------ .../ListView/CustomTemplatesList.tsx | 11 +-- .../ListView/CustomTemplatesListItem.tsx | 1 - .../users/teams/ItemView/Details.tsx | 42 ++++------ .../TeamsDatatable/TeamsDatatable.tsx | 24 ++---- 41 files changed, 369 insertions(+), 727 deletions(-) diff --git a/app/docker/views/configs/configsController.js b/app/docker/views/configs/configsController.js index 0bf7169b0..2d6168172 100644 --- a/app/docker/views/configs/configsController.js +++ b/app/docker/views/configs/configsController.js @@ -1,5 +1,4 @@ import angular from 'angular'; -import { confirmDelete } from '@@/modals/confirm'; class ConfigsController { /* @ngInject */ @@ -34,10 +33,6 @@ class ConfigsController { } async removeAction(selectedItems) { - const confirmed = await confirmDelete('Do you want to remove the selected config(s)?'); - if (!confirmed) { - return null; - } return this.$async(this.removeActionAsync, selectedItems); } diff --git a/app/docker/views/networks/networksController.js b/app/docker/views/networks/networksController.js index 87d4b3797..61249bcde 100644 --- a/app/docker/views/networks/networksController.js +++ b/app/docker/views/networks/networksController.js @@ -1,6 +1,5 @@ import _ from 'lodash-es'; import DockerNetworkHelper from '@/docker/helpers/networkHelper'; -import { confirmDelete } from '@@/modals/confirm'; angular.module('portainer.docker').controller('NetworksController', [ '$q', @@ -13,10 +12,6 @@ angular.module('portainer.docker').controller('NetworksController', [ 'AgentService', function ($q, $scope, $state, NetworkService, Notifications, HttpRequestHelper, endpoint, AgentService) { $scope.removeAction = async function (selectedItems) { - const confirmed = await confirmDelete('Do you want to remove the selected network(s)?'); - if (!confirmed) { - return null; - } var actionCount = selectedItems.length; angular.forEach(selectedItems, function (network) { HttpRequestHelper.setPortainerAgentTargetHeader(network.NodeName); diff --git a/app/docker/views/secrets/secretsController.js b/app/docker/views/secrets/secretsController.js index d59a69472..6b7cb68ca 100644 --- a/app/docker/views/secrets/secretsController.js +++ b/app/docker/views/secrets/secretsController.js @@ -1,4 +1,3 @@ -import { confirmDelete } from '@@/modals/confirm'; angular.module('portainer.docker').controller('SecretsController', [ '$scope', '$state', @@ -6,10 +5,6 @@ angular.module('portainer.docker').controller('SecretsController', [ 'Notifications', function ($scope, $state, SecretService, Notifications) { $scope.removeAction = async function (selectedItems) { - const confirmed = await confirmDelete('Do you want to remove the selected secret(s)?'); - if (!confirmed) { - return null; - } var actionCount = selectedItems.length; angular.forEach(selectedItems, function (secret) { SecretService.remove(secret.Id) diff --git a/app/docker/views/volumes/volumesController.js b/app/docker/views/volumes/volumesController.js index 0ca1352d6..7f9740770 100644 --- a/app/docker/views/volumes/volumesController.js +++ b/app/docker/views/volumes/volumesController.js @@ -1,5 +1,3 @@ -import { confirmDelete } from '@@/modals/confirm'; - angular.module('portainer.docker').controller('VolumesController', [ '$q', '$scope', @@ -13,28 +11,24 @@ angular.module('portainer.docker').controller('VolumesController', [ 'endpoint', function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, Authentication, endpoint) { $scope.removeAction = function (selectedItems) { - confirmDelete('Do you want to remove the selected volume(s)?').then((confirmed) => { - if (confirmed) { - var actionCount = selectedItems.length; - angular.forEach(selectedItems, function (volume) { - HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName); - VolumeService.remove(volume) - .then(function success() { - Notifications.success('Volume successfully removed', volume.Id); - var index = $scope.volumes.indexOf(volume); - $scope.volumes.splice(index, 1); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove volume'); - }) - .finally(function final() { - --actionCount; - if (actionCount === 0) { - $state.reload(); - } - }); + var actionCount = selectedItems.length; + angular.forEach(selectedItems, function (volume) { + HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName); + VolumeService.remove(volume) + .then(function success() { + Notifications.success('Volume successfully removed', volume.Id); + var index = $scope.volumes.indexOf(volume); + $scope.volumes.splice(index, 1); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove volume'); + }) + .finally(function final() { + --actionCount; + if (actionCount === 0) { + $state.reload(); + } }); - } }); }; diff --git a/app/kubernetes/views/applications/applicationsController.js b/app/kubernetes/views/applications/applicationsController.js index 5ac361176..f125cac8b 100644 --- a/app/kubernetes/views/applications/applicationsController.js +++ b/app/kubernetes/views/applications/applicationsController.js @@ -79,11 +79,7 @@ class KubernetesApplicationsController { } removeStacksAction(selectedItems) { - confirmDelete('Are you sure that you want to remove the selected stack(s) ? This will remove all the applications associated to the stack(s).').then((confirmed) => { - if (confirmed) { - return this.$async(this.removeStacksActionAsync, selectedItems); - } - }); + return this.$async(this.removeStacksActionAsync, selectedItems); } async removeActionAsync(selectedItems) { diff --git a/app/portainer/react/views/wizard.ts b/app/portainer/react/views/wizard.ts index 38e327fa4..65b95495e 100644 --- a/app/portainer/react/views/wizard.ts +++ b/app/portainer/react/views/wizard.ts @@ -50,7 +50,7 @@ function config($stateRegistryProvider: StateRegistry) { $stateRegistryProvider.register({ name: 'portainer.wizard.endpoints', - url: '/endpoints', + url: '/endpoints?referrer', views: { 'content@': { component: 'wizardEnvironmentTypeSelectView', diff --git a/app/portainer/views/stacks/stacksController.js b/app/portainer/views/stacks/stacksController.js index 1d7543d55..1a9e2d454 100644 --- a/app/portainer/views/stacks/stacksController.js +++ b/app/portainer/views/stacks/stacksController.js @@ -1,16 +1,9 @@ -import { confirmDelete } from '@@/modals/confirm'; - angular.module('portainer.app').controller('StacksController', StacksController); /* @ngInject */ function StacksController($scope, $state, Notifications, StackService, Authentication, endpoint) { $scope.removeAction = function (selectedItems) { - confirmDelete('Do you want to remove the selected stack(s)? Associated services will be removed as well.').then((confirmed) => { - if (!confirmed) { - return; - } - deleteSelectedStacks(selectedItems); - }); + return deleteSelectedStacks(selectedItems); }; function deleteSelectedStacks(stacks) { diff --git a/app/react/azure/container-instances/ListView/ContainersDatatable.tsx b/app/react/azure/container-instances/ListView/ContainersDatatable.tsx index 3d7f4e872..f207d288b 100644 --- a/app/react/azure/container-instances/ListView/ContainersDatatable.tsx +++ b/app/react/azure/container-instances/ListView/ContainersDatatable.tsx @@ -1,14 +1,13 @@ -import { Box, Plus, Trash2 } from 'lucide-react'; +import { Box } from 'lucide-react'; import { ContainerGroup } from '@/react/azure/types'; import { Authorized } from '@/react/hooks/useUser'; -import { confirmDelete } from '@@/modals/confirm'; import { Datatable } from '@@/datatables'; -import { Button } from '@@/buttons'; -import { Link } from '@@/Link'; +import { AddButton } from '@@/buttons'; import { createPersistedStore } from '@@/datatables/types'; import { useTableState } from '@@/datatables/useTableState'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { columns } from './columns'; @@ -33,36 +32,26 @@ export function ContainersDatatable({ dataset, onRemoveClick }: Props) { getRowId={(container) => container.id} emptyContentLabel="No container available." renderTableActions={(selectedRows) => ( - <> +
- + onConfirmed={() => + handleRemoveClick(selectedRows.map((r) => r.id)) + } + confirmMessage="Are you sure you want to delete the selected containers?" + /> - - - + Add container - +
)} /> ); async function handleRemoveClick(containerIds: string[]) { - const confirmed = await confirmDelete( - 'Are you sure you want to delete the selected containers?' - ); - if (!confirmed) { - return null; - } - return onRemoveClick(containerIds); } } diff --git a/app/react/components/Link.tsx b/app/react/components/Link.tsx index 90571009f..42f012577 100644 --- a/app/react/components/Link.tsx +++ b/app/react/components/Link.tsx @@ -1,5 +1,5 @@ import { PropsWithChildren, AnchorHTMLAttributes } from 'react'; -import { UISref, UISrefProps } from '@uirouter/react'; +import { UISrefProps, useSref } from '@uirouter/react'; interface Props { title?: string; @@ -8,18 +8,18 @@ interface Props { } export function Link({ - title = '', - className, children, + to, + params, + options, ...props }: PropsWithChildren & UISrefProps) { + const { onClick, href } = useSref(to, params, options); + return ( // eslint-disable-next-line react/jsx-props-no-spreading - - {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} - - {children} - - + + {children} + ); } diff --git a/app/react/components/buttons/DeleteButton.tsx b/app/react/components/buttons/DeleteButton.tsx index e2bbef3f0..bd8968f6d 100644 --- a/app/react/components/buttons/DeleteButton.tsx +++ b/app/react/components/buttons/DeleteButton.tsx @@ -1,6 +1,8 @@ import { Trash2 } from 'lucide-react'; import { ComponentProps, PropsWithChildren, ReactNode } from 'react'; +import { AutomationTestingProps } from '@/types'; + import { confirmDelete } from '@@/modals/confirm'; import { Button } from './Button'; @@ -21,13 +23,15 @@ type ConfirmOrClick = export function DeleteButton({ disabled, size, + 'data-cy': dataCy, children, ...props }: PropsWithChildren< - ConfirmOrClick & { - size?: ComponentProps['size']; - disabled?: boolean; - } + AutomationTestingProps & + ConfirmOrClick & { + size?: ComponentProps['size']; + disabled?: boolean; + } >) { return ( diff --git a/app/react/docker/configs/ListView/ConfigsDatatable/ConfigsDatatable.tsx b/app/react/docker/configs/ListView/ConfigsDatatable/ConfigsDatatable.tsx index 7f87933f3..cccc25405 100644 --- a/app/react/docker/configs/ListView/ConfigsDatatable/ConfigsDatatable.tsx +++ b/app/react/docker/configs/ListView/ConfigsDatatable/ConfigsDatatable.tsx @@ -1,13 +1,13 @@ -import { Clipboard, Plus, Trash2 } from 'lucide-react'; +import { Clipboard } from 'lucide-react'; import { Authorized, useAuthorizations } from '@/react/hooks/useUser'; import { Datatable, TableSettingsMenu } from '@@/datatables'; import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh'; import { useRepeater } from '@@/datatables/useRepeater'; -import { Button } from '@@/buttons'; -import { Link } from '@@/Link'; +import { AddButton } from '@@/buttons'; import { useTableState } from '@@/datatables/useTableState'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { DockerConfig } from '../../types'; @@ -17,7 +17,7 @@ import { createStore } from './store'; interface Props { dataset: Array; onRemoveClick: (configs: Array) => void; - onRefresh: () => Promise; + onRefresh: () => void; } const storageKey = 'docker_configs'; @@ -54,24 +54,15 @@ export function ConfigsDatatable({ dataset, onRefresh, onRemoveClick }: Props) { hasWriteAccessQuery.authorized && (
- + onConfirmed={() => onRemoveClick(selectedRows)} + confirmMessage="Do you want to remove the selected config(s)?" + /> - + Add config
) diff --git a/app/react/docker/containers/ListView/ContainersDatatable/ContainersDatatableActions.tsx b/app/react/docker/containers/ListView/ContainersDatatable/ContainersDatatableActions.tsx index 360cc333f..580551073 100644 --- a/app/react/docker/containers/ListView/ContainersDatatable/ContainersDatatableActions.tsx +++ b/app/react/docker/containers/ListView/ContainersDatatable/ContainersDatatableActions.tsx @@ -1,13 +1,5 @@ import { useRouter } from '@uirouter/react'; -import { - Pause, - Play, - Plus, - RefreshCw, - Slash, - Square, - Trash2, -} from 'lucide-react'; +import { Pause, Play, RefreshCw, Slash, Square, Trash2 } from 'lucide-react'; import * as notifications from '@/portainer/services/notifications'; import { useAuthorizations, Authorized } from '@/react/hooks/useUser'; @@ -29,8 +21,7 @@ import { } from '@/react/docker/containers/containers.service'; import type { EnvironmentId } from '@/react/portainer/environments/types'; -import { Link } from '@@/Link'; -import { ButtonGroup, Button } from '@@/buttons'; +import { ButtonGroup, Button, AddButton } from '@@/buttons'; type ContainerServiceAction = ( endpointId: EnvironmentId, @@ -166,11 +157,11 @@ export function ContainersDatatableActions({ {isAddActionVisible && ( - - - - - +
+ + Add container + +
)} ); diff --git a/app/react/docker/images/ListView/ImagesDatatable/ImagesDatatable.tsx b/app/react/docker/images/ListView/ImagesDatatable/ImagesDatatable.tsx index ee408df3f..16fa0ead0 100644 --- a/app/react/docker/images/ListView/ImagesDatatable/ImagesDatatable.tsx +++ b/app/react/docker/images/ListView/ImagesDatatable/ImagesDatatable.tsx @@ -1,11 +1,4 @@ -import { - ChevronDown, - Download, - List, - Plus, - Trash2, - Upload, -} from 'lucide-react'; +import { ChevronDown, Download, List, Trash2, Upload } from 'lucide-react'; import { Menu, MenuButton, MenuItem, MenuPopover } from '@reach/menu-button'; import { positionRight } from '@reach/popover'; import { useMemo } from 'react'; @@ -21,7 +14,7 @@ import { RefreshableTableSettings, } from '@@/datatables/types'; import { useTableState } from '@@/datatables/useTableState'; -import { Button, ButtonGroup, LoadingButton } from '@@/buttons'; +import { AddButton, Button, ButtonGroup, LoadingButton } from '@@/buttons'; import { Link } from '@@/Link'; import { ButtonWithRef } from '@@/buttons/Button'; import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh'; @@ -82,14 +75,12 @@ export function ImagesDatatable({ /> - + )} diff --git a/app/react/docker/networks/ItemView/ItemView.tsx b/app/react/docker/networks/ItemView/ItemView.tsx index a7c396d14..a50f34251 100644 --- a/app/react/docker/networks/ItemView/ItemView.tsx +++ b/app/react/docker/networks/ItemView/ItemView.tsx @@ -8,7 +8,6 @@ import { DockerContainer } from '@/react/docker/containers/types'; import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel'; import { useContainers } from '@/react/docker/containers/queries/containers'; -import { confirmDelete } from '@@/modals/confirm'; import { PageHeader } from '@@/PageHeader'; import { useNetwork, useDeleteNetwork } from '../queries'; @@ -95,19 +94,14 @@ export function ItemView() { ); async function onRemoveNetworkClicked() { - const message = 'Do you want to delete the network?'; - const confirmed = await confirmDelete(message); - - if (confirmed) { - deleteNetworkMutation.mutate( - { environmentId, networkId }, - { - onSuccess: () => { - router.stateService.go('docker.networks'); - }, - } - ); - } + deleteNetworkMutation.mutate( + { environmentId, networkId }, + { + onSuccess: () => { + router.stateService.go('docker.networks'); + }, + } + ); } } diff --git a/app/react/docker/networks/ItemView/NetworkDetailsTable.tsx b/app/react/docker/networks/ItemView/NetworkDetailsTable.tsx index 38a4ac25a..0143131bd 100644 --- a/app/react/docker/networks/ItemView/NetworkDetailsTable.tsx +++ b/app/react/docker/networks/ItemView/NetworkDetailsTable.tsx @@ -1,13 +1,12 @@ import { Fragment } from 'react'; -import { Network, Trash2 } from 'lucide-react'; +import { Network } from 'lucide-react'; import DockerNetworkHelper from '@/docker/helpers/networkHelper'; import { Authorized } from '@/react/hooks/useUser'; import { TableContainer, TableTitle } from '@@/datatables'; import { DetailsTable } from '@@/DetailsTable'; -import { Button } from '@@/buttons'; -import { Icon } from '@@/Icon'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { isSystemNetwork } from '../network.helper'; import { DockerNetwork, IPConfig } from '../types'; @@ -38,21 +37,18 @@ export function NetworkDetailsTable({ {network.Id} {allowRemoveNetwork && ( - - - + + + + Delete this network + + + )} {network.Driver} diff --git a/app/react/docker/networks/ListView/NetworksDatatable.tsx b/app/react/docker/networks/ListView/NetworksDatatable.tsx index f4ad141d7..9c37e5818 100644 --- a/app/react/docker/networks/ListView/NetworksDatatable.tsx +++ b/app/react/docker/networks/ListView/NetworksDatatable.tsx @@ -1,4 +1,4 @@ -import { Plus, Network, Trash2 } from 'lucide-react'; +import { Network } from 'lucide-react'; import { Authorized } from '@/react/hooks/useUser'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; @@ -10,12 +10,12 @@ import { refreshableSettings, RefreshableTableSettings, } from '@@/datatables/types'; -import { Button } from '@@/buttons'; +import { AddButton } from '@@/buttons'; import { TableSettingsMenu } from '@@/datatables'; import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh'; import { useRepeater } from '@@/datatables/useRepeater'; import { useTableState } from '@@/datatables/useTableState'; -import { Link } from '@@/Link'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { useIsSwarm } from '../../proxy/queries/useInfo'; @@ -80,22 +80,16 @@ export function NetworksDatatable({ dataset, onRemove, onRefresh }: Props) { - + confirmMessage="Do you want to remove the selected network(s)?" + onConfirmed={() => onRemove(selectedRows)} + /> - - + )} diff --git a/app/react/docker/secrets/ListView/SecretsDatatable.tsx b/app/react/docker/secrets/ListView/SecretsDatatable.tsx index 81343a4e1..cc53ddac1 100644 --- a/app/react/docker/secrets/ListView/SecretsDatatable.tsx +++ b/app/react/docker/secrets/ListView/SecretsDatatable.tsx @@ -1,5 +1,5 @@ import { createColumnHelper } from '@tanstack/react-table'; -import { Lock, Plus, Trash2 } from 'lucide-react'; +import { Lock } from 'lucide-react'; import { SecretViewModel } from '@/docker/models/secret'; import { isoDate } from '@/portainer/filters/filters'; @@ -15,9 +15,9 @@ import { } from '@@/datatables/types'; import { useTableState } from '@@/datatables/useTableState'; import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh'; -import { Button } from '@@/buttons'; -import { Link } from '@@/Link'; +import { AddButton } from '@@/buttons'; import { useRepeater } from '@@/datatables/useRepeater'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { createOwnershipColumn } from '../../components/datatable/createOwnershipColumn'; @@ -96,28 +96,16 @@ function TableActions({ return (
- + /> - + Add secret
); diff --git a/app/react/docker/services/ListView/ServicesDatatable/TableActions.tsx b/app/react/docker/services/ListView/ServicesDatatable/TableActions.tsx index 8593ead6f..147cc9b95 100644 --- a/app/react/docker/services/ListView/ServicesDatatable/TableActions.tsx +++ b/app/react/docker/services/ListView/ServicesDatatable/TableActions.tsx @@ -1,4 +1,4 @@ -import { Trash2, Plus, RefreshCw } from 'lucide-react'; +import { RefreshCw } from 'lucide-react'; import { useRouter } from '@uirouter/react'; import { ServiceViewModel } from '@/docker/models/service'; @@ -6,9 +6,8 @@ import { Authorized } from '@/react/hooks/useUser'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { notifySuccess } from '@/portainer/services/notifications'; -import { Link } from '@@/Link'; -import { Button, ButtonGroup } from '@@/buttons'; -import { confirmDelete } from '@@/modals/confirm'; +import { AddButton, Button, ButtonGroup } from '@@/buttons'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { confirmServiceForceUpdate } from '../../common/update-service-modal'; @@ -46,28 +45,18 @@ export function TableActions({ )} - + /> {isAddActionVisible && ( - + Add service )} @@ -97,14 +86,6 @@ export function TableActions({ } async function handleRemove(selectedItems: Array) { - const confirmed = await confirmDelete( - 'Do you want to remove the selected service(s)? All the containers associated to the selected service(s) will be removed too.' - ); - - if (!confirmed) { - return; - } - removeMutation.mutate( selectedItems.map((service) => service.Id), { diff --git a/app/react/docker/stacks/ListView/StacksDatatable/TableActions.tsx b/app/react/docker/stacks/ListView/StacksDatatable/TableActions.tsx index f12542b3d..30693d39d 100644 --- a/app/react/docker/stacks/ListView/StacksDatatable/TableActions.tsx +++ b/app/react/docker/stacks/ListView/StacksDatatable/TableActions.tsx @@ -1,9 +1,7 @@ -import { Trash2, Plus } from 'lucide-react'; - import { Authorized } from '@/react/hooks/useUser'; -import { Link } from '@@/Link'; -import { Button } from '@@/buttons'; +import { AddButton } from '@@/buttons'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { DecoratedStack } from './types'; @@ -17,28 +15,18 @@ export function TableActions({ return (
- + /> - +
); diff --git a/app/react/docker/volumes/ListView/VolumesDatatable/TableActions.tsx b/app/react/docker/volumes/ListView/VolumesDatatable/TableActions.tsx index 49c63a5a9..93e0acd45 100644 --- a/app/react/docker/volumes/ListView/VolumesDatatable/TableActions.tsx +++ b/app/react/docker/volumes/ListView/VolumesDatatable/TableActions.tsx @@ -1,9 +1,7 @@ -import { Plus, Trash2 } from 'lucide-react'; - import { Authorized } from '@/react/hooks/useUser'; -import { Link } from '@@/Link'; -import { Button } from '@@/buttons'; +import { AddButton } from '@@/buttons'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { DecoratedVolume } from '../types'; @@ -17,27 +15,15 @@ export function TableActions({ return (
- + /> - + Add volume
); diff --git a/app/react/edge/edge-devices/WaitingRoomView/Datatable/TableActions.tsx b/app/react/edge/edge-devices/WaitingRoomView/Datatable/TableActions.tsx index 81f01af17..98b2db24e 100644 --- a/app/react/edge/edge-devices/WaitingRoomView/Datatable/TableActions.tsx +++ b/app/react/edge/edge-devices/WaitingRoomView/Datatable/TableActions.tsx @@ -1,4 +1,4 @@ -import { Check, CheckCircle, Trash2 } from 'lucide-react'; +import { Check, CheckCircle } from 'lucide-react'; import { notifySuccess } from '@/portainer/services/notifications'; import { useDeleteEnvironmentsMutation } from '@/react/portainer/environments/queries/useDeleteEnvironmentsMutation'; @@ -7,10 +7,9 @@ import { withReactQuery } from '@/react-tools/withReactQuery'; import { useIsPureAdmin } from '@/react/hooks/useUser'; import { Button } from '@@/buttons'; -import { ModalType, openModal } from '@@/modals'; -import { confirm } from '@@/modals/confirm'; -import { buildConfirmButton } from '@@/modals/utils'; +import { openModal } from '@@/modals'; import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { useAssociateDeviceMutation, useLicenseOverused } from '../queries'; import { WaitingRoomEnvironment } from '../types'; @@ -36,14 +35,13 @@ export function TableActions({ return ( <> - + d.Id), { diff --git a/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/TableActions.tsx b/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/TableActions.tsx index 53a2254d6..579060dc6 100644 --- a/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/TableActions.tsx +++ b/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/TableActions.tsx @@ -1,11 +1,7 @@ -import { Trash2, Plus } from 'lucide-react'; - import { notifySuccess } from '@/portainer/services/notifications'; -import { Button } from '@@/buttons'; -import { confirmDestructive } from '@@/modals/confirm'; -import { buildConfirmButton } from '@@/modals/utils'; -import { Link } from '@@/Link'; +import { AddButton } from '@@/buttons'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { useDeleteEdgeStacksMutation } from './useDeleteEdgeStacksMutation'; import { DecoratedEdgeStack } from './types'; @@ -19,39 +15,17 @@ export function TableActions({ return (
- + onConfirmed={() => handleRemove(selectedItems)} + confirmMessage="Are you sure you want to remove the selected Edge stack(s)?" + /> - + Add stack
); async function handleRemove(selectedItems: Array) { - const confirmed = await confirmDestructive({ - title: 'Are you sure?', - message: 'Are you sure you want to remove the selected Edge stack(s)?', - confirmButton: buildConfirmButton('Remove', 'danger'), - }); - - if (!confirmed) { - return; - } - const ids = selectedItems.map((item) => item.Id); removeMutation.mutate(ids, { onSuccess: () => { diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationDetailsWidget.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationDetailsWidget.tsx index 46ce98d90..fcb705263 100644 --- a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationDetailsWidget.tsx +++ b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationDetailsWidget.tsx @@ -1,4 +1,4 @@ -import { Pencil, Plus } from 'lucide-react'; +import { Pencil } from 'lucide-react'; import { useCurrentStateAndParams } from '@uirouter/react'; import { Pod } from 'kubernetes-types/core/v1'; @@ -7,7 +7,7 @@ import { useStackFile } from '@/react/common/stacks/stack.service'; import { useNamespaceQuery } from '@/react/kubernetes/namespaces/queries/useNamespaceQuery'; import { Widget, WidgetBody } from '@@/Widget'; -import { Button } from '@@/buttons'; +import { AddButton, Button } from '@@/buttons'; import { Link } from '@@/Link'; import { Icon } from '@@/Icon'; @@ -102,23 +102,15 @@ export function ApplicationDetailsWidget() { /> )} {appStackFileQuery.data && ( - - - + Create template from application + )} )} diff --git a/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/TableActions.tsx b/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/TableActions.tsx index fb7ef3546..fa7a3c647 100644 --- a/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/TableActions.tsx +++ b/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/TableActions.tsx @@ -1,8 +1,6 @@ -import { Trash2 } from 'lucide-react'; - import { Authorized } from '@/react/hooks/useUser'; -import { Button } from '@@/buttons'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { KubernetesStack } from '../../types'; @@ -15,15 +13,12 @@ export function TableActions({ }) { return ( - + /> ); } diff --git a/app/react/kubernetes/configs/ListView/ConfigMapsDatatable/ConfigMapsDatatable.tsx b/app/react/kubernetes/configs/ListView/ConfigMapsDatatable/ConfigMapsDatatable.tsx index 961661870..a96f71e63 100644 --- a/app/react/kubernetes/configs/ListView/ConfigMapsDatatable/ConfigMapsDatatable.tsx +++ b/app/react/kubernetes/configs/ListView/ConfigMapsDatatable/ConfigMapsDatatable.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { FileCode, Plus, Trash2 } from 'lucide-react'; +import { FileCode } from 'lucide-react'; import { ConfigMap } from 'kubernetes-types/core/v1'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; @@ -12,12 +12,12 @@ import { Application } from '@/react/kubernetes/applications/types'; import { pluralize } from '@/portainer/helpers/strings'; import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery'; import { Namespaces } from '@/react/kubernetes/namespaces/types'; +import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton'; import { Datatable, TableSettingsMenu } from '@@/datatables'; -import { confirmDelete } from '@@/modals/confirm'; -import { Button } from '@@/buttons'; -import { Link } from '@@/Link'; +import { AddButton } from '@@/buttons'; import { useTableState } from '@@/datatables/useTableState'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { useConfigMapsForCluster, @@ -139,16 +139,6 @@ function TableActions({ const deleteConfigMapMutation = useMutationDeleteConfigMaps(environmentId); async function handleRemoveClick(configMaps: ConfigMap[]) { - const confirmed = await confirmDelete( - `Are you sure you want to remove the selected ${pluralize( - configMaps.length, - 'ConfigMap' - )}?` - ); - if (!confirmed) { - return; - } - const configMapsToDelete = configMaps.map((configMap) => ({ namespace: configMap.metadata?.namespace ?? '', name: configMap.metadata?.name ?? '', @@ -159,41 +149,30 @@ function TableActions({ return ( - - - - - + + - - + /> ); } diff --git a/app/react/kubernetes/configs/ListView/SecretsDatatable/SecretsDatatable.tsx b/app/react/kubernetes/configs/ListView/SecretsDatatable/SecretsDatatable.tsx index 7f0699f9d..318c3d4ec 100644 --- a/app/react/kubernetes/configs/ListView/SecretsDatatable/SecretsDatatable.tsx +++ b/app/react/kubernetes/configs/ListView/SecretsDatatable/SecretsDatatable.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { Lock, Plus, Trash2 } from 'lucide-react'; +import { Lock } from 'lucide-react'; import { Secret } from 'kubernetes-types/core/v1'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; @@ -12,12 +12,12 @@ import { Application } from '@/react/kubernetes/applications/types'; import { pluralize } from '@/portainer/helpers/strings'; import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery'; import { Namespaces } from '@/react/kubernetes/namespaces/types'; +import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton'; import { Datatable, TableSettingsMenu } from '@@/datatables'; -import { confirmDelete } from '@@/modals/confirm'; -import { Button } from '@@/buttons'; -import { Link } from '@@/Link'; +import { AddButton } from '@@/buttons'; import { useTableState } from '@@/datatables/useTableState'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { useSecretsForCluster, @@ -135,16 +135,6 @@ function TableActions({ selectedItems }: { selectedItems: SecretRowData[] }) { const deleteSecretMutation = useMutationDeleteSecrets(environmentId); async function handleRemoveClick(secrets: SecretRowData[]) { - const confirmed = await confirmDelete( - `Are you sure you want to remove the selected ${pluralize( - secrets.length, - 'secret' - )}?` - ); - if (!confirmed) { - return; - } - const secretsToDelete = secrets.map((secret) => ({ namespace: secret.metadata?.namespace ?? '', name: secret.metadata?.name ?? '', @@ -155,41 +145,28 @@ function TableActions({ selectedItems }: { selectedItems: SecretRowData[] }) { return ( - - - - - + - - + /> ); } diff --git a/app/react/kubernetes/ingresses/IngressDatatable/IngressDatatable.tsx b/app/react/kubernetes/ingresses/IngressDatatable/IngressDatatable.tsx index 6e9496f94..062487dbd 100644 --- a/app/react/kubernetes/ingresses/IngressDatatable/IngressDatatable.tsx +++ b/app/react/kubernetes/ingresses/IngressDatatable/IngressDatatable.tsx @@ -1,4 +1,3 @@ -import { Plus, Trash2 } from 'lucide-react'; import { useRouter } from '@uirouter/react'; import { useMemo } from 'react'; @@ -9,16 +8,16 @@ import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultD import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store'; import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription'; -import { confirmDelete } from '@@/modals/confirm'; import { Datatable, TableSettingsMenu } from '@@/datatables'; -import { Button } from '@@/buttons'; -import { Link } from '@@/Link'; +import { AddButton } from '@@/buttons'; import { useTableState } from '@@/datatables/useTableState'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { DeleteIngressesRequest, Ingress } from '../types'; import { useDeleteIngresses, useIngresses } from '../queries'; import { useNamespacesQuery } from '../../namespaces/queries/useNamespacesQuery'; import { Namespaces } from '../../namespaces/types'; +import { CreateFromManifestButton } from '../../components/CreateFromManifestButton'; import { columns } from './columns'; @@ -111,38 +110,20 @@ export function IngressDatatable() { function tableActions(selectedFlatRows: Ingress[]) { return ( -
- - - + + handleRemoveClick(selectedFlatRows)} + data-cy="k8sSecret-removeSecretButton" + confirmMessage="Are you sure you want to delete the selected ingresses?" + /> - - - - - - - - - - -
+ + Add with form + + + + ); } @@ -152,13 +133,6 @@ export function IngressDatatable() { } async function handleRemoveClick(ingresses: SelectedIngress[]) { - const confirmed = await confirmDelete( - 'Are you sure you want to delete the selected ingresses?' - ); - if (!confirmed) { - return null; - } - const payload: DeleteIngressesRequest = {} as DeleteIngressesRequest; ingresses.forEach((ingress) => { payload[ingress.Namespace] = payload[ingress.Namespace] || []; @@ -173,6 +147,5 @@ export function IngressDatatable() { }, } ); - return ingresses; } } diff --git a/app/react/kubernetes/ingresses/style.css b/app/react/kubernetes/ingresses/style.css index 928aeecb8..7f189cc10 100644 --- a/app/react/kubernetes/ingresses/style.css +++ b/app/react/kubernetes/ingresses/style.css @@ -7,7 +7,6 @@ background-color: var(--bg-body-color); } -.ingressDatatable-actions button > span, .anntation-actions button > span, .rules-action button > span, .rule button > span { diff --git a/app/react/kubernetes/services/ServicesView/ServicesDatatable/ServicesDatatable.tsx b/app/react/kubernetes/services/ServicesView/ServicesDatatable/ServicesDatatable.tsx index fe3389ba1..23d04db35 100644 --- a/app/react/kubernetes/services/ServicesView/ServicesDatatable/ServicesDatatable.tsx +++ b/app/react/kubernetes/services/ServicesView/ServicesDatatable/ServicesDatatable.tsx @@ -1,8 +1,8 @@ -import { useMemo } from 'react'; -import { Shuffle, Trash2 } from 'lucide-react'; +import { Shuffle } from 'lucide-react'; import { useRouter } from '@uirouter/react'; import clsx from 'clsx'; import { Row } from '@tanstack/react-table'; +import { useMemo } from 'react'; import { Namespaces } from '@/react/kubernetes/namespaces/types'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; @@ -12,12 +12,11 @@ import { pluralize } from '@/portainer/helpers/strings'; import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings'; import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription'; import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery'; +import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton'; import { Datatable, Table, TableSettingsMenu } from '@@/datatables'; -import { confirmDelete } from '@@/modals/confirm'; -import { Button } from '@@/buttons'; -import { Link } from '@@/Link'; import { useTableState } from '@@/datatables/useTableState'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { useMutationDeleteServices, @@ -94,7 +93,7 @@ export function ServicesDatatable() { ); } -// useServicesRowData appends the `isSyetem` property to the service data +// useServicesRowData appends the `isSystem` property to the service data function useServicesRowData( services: Service[], namespaces?: Namespaces @@ -136,26 +135,33 @@ function TableActions({ selectedItems }: TableActionsProps) { const deleteServicesMutation = useMutationDeleteServices(environmentId); const router = useRouter(); - async function handleRemoveClick(services: SelectedService[]) { - const confirmed = await confirmDelete( - <> -

{`Are you sure you want to remove the selected ${pluralize( - services.length, - 'service' - )}?`}

-
    - {services.map((s, index) => ( -
  • - {s.Namespace}/{s.Name} -
  • - ))} -
- - ); - if (!confirmed) { - return null; - } + return ( + + handleRemoveClick(selectedItems)} + confirmMessage={ + <> +

{`Are you sure you want to remove the selected ${pluralize( + selectedItems.length, + 'service' + )}?`}

+
    + {selectedItems.map((s, index) => ( +
  • + {s.Namespace}/{s.Name} +
  • + ))} +
+ + } + /> + +
+ ); + + async function handleRemoveClick(services: SelectedService[]) { const payload: Record = {}; services.forEach((service) => { payload[service.Namespace] = payload[service.Namespace] || []; @@ -181,32 +187,5 @@ function TableActions({ selectedItems }: TableActionsProps) { }, } ); - return services; } - - return ( -
- - - - - - - -
- ); } diff --git a/app/react/portainer/account/AccountView/HelmRepositoryDatatable/HelmRepositoryDatatableActions.tsx b/app/react/portainer/account/AccountView/HelmRepositoryDatatable/HelmRepositoryDatatableActions.tsx index da73d8490..8a2408e39 100644 --- a/app/react/portainer/account/AccountView/HelmRepositoryDatatable/HelmRepositoryDatatableActions.tsx +++ b/app/react/portainer/account/AccountView/HelmRepositoryDatatable/HelmRepositoryDatatableActions.tsx @@ -1,10 +1,9 @@ import { useRouter } from '@uirouter/react'; -import { Trash2 } from 'lucide-react'; import { pluralize } from '@/portainer/helpers/strings'; -import { confirmDestructive } from '@@/modals/confirm'; -import { AddButton, Button } from '@@/buttons'; +import { AddButton } from '@@/buttons'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { HelmRepository } from './types'; import { useDeleteHelmRepositoriesMutation } from './helm-repositories.service'; @@ -18,37 +17,27 @@ export function HelmRepositoryDatatableActions({ selectedItems }: Props) { const deleteHelmRepoMutation = useDeleteHelmRepositoriesMutation(); return ( -
- - - Add Helm repository -
+ ); async function onDeleteClick(selectedItems: HelmRepository[]) { - const confirmed = await confirmDestructive({ - title: 'Confirm action', - message: `Are you sure you want to remove the selected Helm ${pluralize( - selectedItems.length, - 'repository', - 'repositories' - )}?`, - }); - - if (!confirmed) { - return; - } - deleteHelmRepoMutation.mutate(selectedItems, { onSuccess: () => { router.stateService.reload(); diff --git a/app/react/portainer/environments/ListView/EnvironmentsDatatable.tsx b/app/react/portainer/environments/ListView/EnvironmentsDatatable.tsx index 1b17a818c..02ea17876 100644 --- a/app/react/portainer/environments/ListView/EnvironmentsDatatable.tsx +++ b/app/react/portainer/environments/ListView/EnvironmentsDatatable.tsx @@ -1,4 +1,4 @@ -import { HardDrive, Plus, Trash2 } from 'lucide-react'; +import { HardDrive, Trash2 } from 'lucide-react'; import { useState } from 'react'; import { useEnvironmentList } from '@/react/portainer/environments/queries'; @@ -6,8 +6,7 @@ import { useGroups } from '@/react/portainer/environments/environment-groups/que import { Datatable } from '@@/datatables'; import { createPersistedStore } from '@@/datatables/types'; -import { Button } from '@@/buttons'; -import { Link } from '@@/Link'; +import { AddButton, Button } from '@@/buttons'; import { useTableState } from '@@/datatables/useTableState'; import { isBE } from '../../feature-flags/feature-flags.service'; @@ -86,26 +85,20 @@ export function EnvironmentsDatatable({ {isBE && ( - + )} - - - + + + Add environment + )} /> diff --git a/app/react/portainer/environments/ListView/ImportFdoDeviceButton.tsx b/app/react/portainer/environments/ListView/ImportFdoDeviceButton.tsx index 8683155da..d42efe20a 100644 --- a/app/react/portainer/environments/ListView/ImportFdoDeviceButton.tsx +++ b/app/react/portainer/environments/ListView/ImportFdoDeviceButton.tsx @@ -1,7 +1,4 @@ -import { Plus } from 'lucide-react'; - -import { Button } from '@@/buttons'; -import { Link } from '@@/Link'; +import { AddButton } from '@@/buttons'; import { useSettings } from '../../settings/queries'; import { @@ -22,15 +19,10 @@ export function ImportFdoDeviceButton() { } return ( - +
+ + Import FDO device + +
); } diff --git a/app/react/portainer/environments/update-schedules/ListView/ListView.tsx b/app/react/portainer/environments/update-schedules/ListView/ListView.tsx index 35b35a02d..c0d16fea2 100644 --- a/app/react/portainer/environments/update-schedules/ListView/ListView.tsx +++ b/app/react/portainer/environments/update-schedules/ListView/ListView.tsx @@ -1,4 +1,4 @@ -import { Clock, Trash2 } from 'lucide-react'; +import { Clock } from 'lucide-react'; import { useMemo } from 'react'; import _ from 'lodash'; @@ -6,12 +6,11 @@ import { notifySuccess } from '@/portainer/services/notifications'; import { withLimitToBE } from '@/react/hooks/useLimitToBE'; import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups'; -import { confirmDelete } from '@@/modals/confirm'; import { Datatable } from '@@/datatables'; import { PageHeader } from '@@/PageHeader'; -import { Button } from '@@/buttons'; -import { Link } from '@@/Link'; +import { AddButton } from '@@/buttons'; import { useTableState } from '@@/datatables/useTableState'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { useList } from '../queries/list'; import { EdgeUpdateSchedule, StatusType } from '../types'; @@ -90,29 +89,16 @@ function TableActions({ const removeMutation = useRemoveMutation(); return ( <> - - - - - + confirmMessage="Are you sure you want to remove these schedules?" + /> + Add update & rollback schedule ); async function handleRemove() { - const confirmed = await confirmDelete( - 'Are you sure you want to remove these?' - ); - if (!confirmed) { - return; - } - removeMutation.mutate(selectedRows, { onSuccess: () => { notifySuccess('Success', 'Schedules successfully removed'); diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx index 427d4eb44..6f1bf1c1f 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx @@ -33,7 +33,7 @@ import { WizardEndpointsList } from './WizardEndpointsList'; export function EnvironmentCreationView() { const { - params: { localEndpointId: localEndpointIdParam }, + params: { localEndpointId: localEndpointIdParam, referrer }, } = useCurrentStateAndParams(); const [environmentIds, setEnvironmentIds] = useState(() => { @@ -130,8 +130,7 @@ export function EnvironmentCreationView() { ]) ), }); - if (localStorage.getItem('wizardReferrer') === 'environments') { - localStorage.removeItem('wizardReferrer'); + if (referrer === 'environments') { router.stateService.go('portainer.endpoints'); return; } diff --git a/app/react/portainer/notifications/NotificationsView.tsx b/app/react/portainer/notifications/NotificationsView.tsx index 6024197d4..afb3b69d0 100644 --- a/app/react/portainer/notifications/NotificationsView.tsx +++ b/app/react/portainer/notifications/NotificationsView.tsx @@ -1,4 +1,4 @@ -import { Bell, Trash2 } from 'lucide-react'; +import { Bell } from 'lucide-react'; import { useStore } from 'zustand'; import { useCurrentStateAndParams } from '@uirouter/react'; @@ -10,9 +10,9 @@ import { withReactQuery } from '@/react-tools/withReactQuery'; import { PageHeader } from '@@/PageHeader'; import { Datatable } from '@@/datatables'; -import { Button } from '@@/buttons'; import { createPersistedStore } from '@@/datatables/types'; import { useTableState } from '@@/datatables/useTableState'; +import { DeleteButton } from '@@/buttons/DeleteButton'; import { notificationsStore } from './notifications-store'; import { ToastNotification } from './types'; @@ -62,14 +62,11 @@ function TableActions({ selectedRows }: { selectedRows: ToastNotification[] }) { const { user } = useUser(); const notificationsStoreState = useStore(notificationsStore); return ( - + confirmMessage="Are you sure you want to remove the selected notifications?" + /> ); function handleRemove() { diff --git a/app/react/portainer/settings/EdgeComputeView/FDOProfilesDatatable/FDOProfilesDatatableActions.tsx b/app/react/portainer/settings/EdgeComputeView/FDOProfilesDatatable/FDOProfilesDatatableActions.tsx index a3b7759aa..1b78e96b5 100644 --- a/app/react/portainer/settings/EdgeComputeView/FDOProfilesDatatable/FDOProfilesDatatableActions.tsx +++ b/app/react/portainer/settings/EdgeComputeView/FDOProfilesDatatable/FDOProfilesDatatableActions.tsx @@ -1,6 +1,6 @@ import { useQueryClient } from 'react-query'; import { useRouter } from '@uirouter/react'; -import { PlusCircle, Trash2 } from 'lucide-react'; +import { PlusCircle } from 'lucide-react'; import { Profile } from '@/portainer/hostmanagement/fdo/model'; import * as notifications from '@/portainer/services/notifications'; @@ -9,10 +9,10 @@ import { duplicateProfile, } from '@/portainer/hostmanagement/fdo/fdo.service'; -import { confirm, confirmDestructive } from '@@/modals/confirm'; +import { confirm } from '@@/modals/confirm'; import { Link } from '@@/Link'; import { Button } from '@@/buttons'; -import { buildConfirmButton } from '@@/modals/utils'; +import { DeleteButton } from '@@/buttons/DeleteButton'; interface Props { isFDOEnabled: boolean; @@ -27,7 +27,7 @@ export function FDOProfilesDatatableActions({ const queryClient = useQueryClient(); return ( -
+ <> - -
+ onDeleteProfileClick()} + confirmMessage="This action will delete the selected profile(s). Continue?" + /> + ); async function onDuplicateProfileClick() { @@ -80,16 +77,6 @@ export function FDOProfilesDatatableActions({ } async function onDeleteProfileClick() { - const confirmed = await confirmDestructive({ - title: 'Are you sure?', - message: 'This action will delete the selected profile(s). Continue?', - confirmButton: buildConfirmButton('Remove', 'danger'), - }); - - if (!confirmed) { - return; - } - await Promise.all( selectedItems.map(async (profile) => { try { diff --git a/app/react/portainer/templates/app-templates/AppTemplatesListItem.test.tsx b/app/react/portainer/templates/app-templates/AppTemplatesListItem.test.tsx index e5221b8fa..0857d3ef2 100644 --- a/app/react/portainer/templates/app-templates/AppTemplatesListItem.test.tsx +++ b/app/react/portainer/templates/app-templates/AppTemplatesListItem.test.tsx @@ -2,7 +2,9 @@ import userEvent from '@testing-library/user-event'; import { PropsWithChildren } from 'react'; import { render } from '@testing-library/react'; -import { AppTemplatesListItem } from './AppTemplatesListItem'; +import { withTestRouter } from '@/react/test-utils/withRouter'; + +import { AppTemplatesListItem as BaseComponent } from './AppTemplatesListItem'; import { TemplateViewModel } from './view-model'; import { TemplateType } from './types'; @@ -15,13 +17,7 @@ test('should render AppTemplatesListItem component', () => { const onSelect = vi.fn(); const isSelected = false; - const { getByText } = render( - - ); + const { getByText } = renderComponent({ isSelected, template, onSelect }); expect(getByText(template.Title, { exact: false })).toBeInTheDocument(); }); @@ -45,26 +41,23 @@ const copyAsCustomTestCases = [ vi.mock('@uirouter/react', async (importOriginal: () => Promise) => ({ ...(await importOriginal()), UISref: ({ children }: PropsWithChildren) => children, // Mocking UISref to render its children directly + useSref: () => ({ href: '' }), // Mocking useSref to return an empty string })); copyAsCustomTestCases.forEach(({ type, expected }) => { test(`copy as custom button should ${ expected ? '' : 'not ' - }be rendered for type ${type}`, () => { + }be rendered for type ${TemplateType[type]}`, () => { const onSelect = vi.fn(); const isSelected = false; - const { queryByText, unmount } = render( - - ); + const { queryByText, unmount } = renderComponent({ + isSelected, + template: { + Type: type, + } as TemplateViewModel, + onSelect, + }); if (expected) { expect(queryByText('Copy as Custom')).toBeVisible(); @@ -86,16 +79,34 @@ test('should call onSelect when clicked', async () => { const onSelect = vi.fn(); const isSelected = false; - const { getByLabelText } = render( - - ); + const { getByLabelText } = renderComponent({ + isSelected, + template, + onSelect, + }); const button = getByLabelText(template.Title); await user.click(button); expect(onSelect).toHaveBeenCalledWith(template); }); + +function renderComponent({ + isSelected = false, + onSelect, + template, +}: { + template: TemplateViewModel; + onSelect?: () => void; + isSelected?: boolean; +}) { + const AppTemplatesListItem = withTestRouter(BaseComponent); + + return render( + + ); +} diff --git a/app/react/portainer/templates/custom-templates/ListView/CustomTemplatesList.tsx b/app/react/portainer/templates/custom-templates/ListView/CustomTemplatesList.tsx index ae485479b..f01f26e2f 100644 --- a/app/react/portainer/templates/custom-templates/ListView/CustomTemplatesList.tsx +++ b/app/react/portainer/templates/custom-templates/ListView/CustomTemplatesList.tsx @@ -1,4 +1,4 @@ -import { Edit, Plus } from 'lucide-react'; +import { Edit } from 'lucide-react'; import _ from 'lodash'; import { useCallback, useState } from 'react'; @@ -9,8 +9,7 @@ import { Table } from '@@/datatables'; import { useTableState } from '@@/datatables/useTableState'; import { createPersistedStore } from '@@/datatables/types'; import { DatatableFooter } from '@@/datatables/DatatableFooter'; -import { Button } from '@@/buttons'; -import { Link } from '@@/Link'; +import { AddButton } from '@@/buttons'; import { CustomTemplatesListItem } from './CustomTemplatesListItem'; @@ -56,11 +55,7 @@ export function CustomTemplatesList({ searchValue={listState.search} title="Custom Templates" titleIcon={Edit} - renderTableActions={() => ( - - )} + renderTableActions={() => Add Custom Template} />
diff --git a/app/react/portainer/templates/custom-templates/ListView/CustomTemplatesListItem.tsx b/app/react/portainer/templates/custom-templates/ListView/CustomTemplatesListItem.tsx index 95f051a36..309708085 100644 --- a/app/react/portainer/templates/custom-templates/ListView/CustomTemplatesListItem.tsx +++ b/app/react/portainer/templates/custom-templates/ListView/CustomTemplatesListItem.tsx @@ -46,7 +46,6 @@ export function CustomTemplatesListItem({ - )} +
+ {!teamSyncQuery.data && team.Name} + {isAdmin && ( + + Delete this team + + )} +
@@ -75,18 +75,8 @@ export function Details({ team, memberships, isAdmin }: Props) { ); async function handleDeleteClick() { - const confirmed = await confirmDelete( - `Do you want to delete this team? Users in this team will not be deleted.` - ); - if (!confirmed) { - return; - } - - deleteMutation.mutate(team.Id, { - onSuccess() { - router.stateService.go('portainer.teams'); - }, - }); + router.stateService.go('portainer.teams'); + deleteMutation.mutate(team.Id); } } diff --git a/app/react/portainer/users/teams/ListView/TeamsDatatable/TeamsDatatable.tsx b/app/react/portainer/users/teams/ListView/TeamsDatatable/TeamsDatatable.tsx index a8f277385..8b6f53ec2 100644 --- a/app/react/portainer/users/teams/ListView/TeamsDatatable/TeamsDatatable.tsx +++ b/app/react/portainer/users/teams/ListView/TeamsDatatable/TeamsDatatable.tsx @@ -1,5 +1,5 @@ import { useMutation, useQueryClient } from 'react-query'; -import { Trash2, Users } from 'lucide-react'; +import { Users } from 'lucide-react'; import { ColumnDef } from '@tanstack/react-table'; import { notifySuccess } from '@/portainer/services/notifications'; @@ -7,12 +7,11 @@ import { promiseSequence } from '@/portainer/helpers/promise-utils'; import { Team, TeamId } from '@/react/portainer/users/teams/types'; import { deleteTeam } from '@/react/portainer/users/teams/teams.service'; -import { confirmDelete } from '@@/modals/confirm'; import { Datatable } from '@@/datatables'; -import { Button } from '@@/buttons'; import { buildNameColumn } from '@@/datatables/buildNameColumn'; import { createPersistedStore } from '@@/datatables/types'; import { useTableState } from '@@/datatables/useTableState'; +import { DeleteButton } from '@@/buttons/DeleteButton'; const storageKey = 'teams'; @@ -40,14 +39,11 @@ export function TeamsDatatable({ teams, isAdmin }: Props) { titleIcon={Users} renderTableActions={(selectedRows) => isAdmin && ( - + confirmMessage="Are you sure you want to remove the selected teams?" + /> ) } emptyContentLabel="No teams found" @@ -79,14 +75,6 @@ function useRemoveMutation() { return { handleRemove }; async function handleRemove(teams: TeamId[]) { - const confirmed = await confirmDelete( - 'Are you sure you want to remove the selected teams?' - ); - - if (!confirmed) { - return; - } - deleteMutation.mutate(teams, { onSuccess: () => { notifySuccess('Teams successfully removed', '');