refactor(tables): use add and delete buttons [EE-6297] (#10668)

Co-authored-by: Chaim Lev-Ari <chaim.levi-ari@portaienr.io>
pull/10840/head
Chaim Lev-Ari 2024-04-08 17:21:41 +03:00 committed by GitHub
parent d88ef03ddb
commit 9600eb6fa1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 369 additions and 727 deletions

View File

@ -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);
}

View File

@ -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);

View File

@ -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)

View File

@ -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();
}
});
}
});
};

View File

@ -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) {

View File

@ -50,7 +50,7 @@ function config($stateRegistryProvider: StateRegistry) {
$stateRegistryProvider.register({
name: 'portainer.wizard.endpoints',
url: '/endpoints',
url: '/endpoints?referrer',
views: {
'content@': {
component: 'wizardEnvironmentTypeSelectView',

View File

@ -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) {

View File

@ -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) => (
<>
<div className="flex gap-2">
<Authorized authorizations="AzureContainerGroupDelete">
<Button
color="dangerlight"
<DeleteButton
disabled={selectedRows.length === 0}
onClick={() => handleRemoveClick(selectedRows.map((r) => r.id))}
icon={Trash2}
>
Remove
</Button>
onConfirmed={() =>
handleRemoveClick(selectedRows.map((r) => r.id))
}
confirmMessage="Are you sure you want to delete the selected containers?"
/>
</Authorized>
<Authorized authorizations="AzureContainerGroupCreate">
<Link to="azure.containerinstances.new" className="space-left">
<Button icon={Plus}>Add container</Button>
</Link>
<AddButton>Add container</AddButton>
</Authorized>
</>
</div>
)}
/>
);
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);
}
}

View File

@ -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<Props> & UISrefProps) {
const { onClick, href } = useSref(to, params, options);
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<UISref className={className} {...props}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a title={title} target={props.target} rel={props.rel}>
{children}
</a>
</UISref>
<a onClick={onClick} href={href} {...props}>
{children}
</a>
);
}

View File

@ -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<typeof Button>['size'];
disabled?: boolean;
}
AutomationTestingProps &
ConfirmOrClick & {
size?: ComponentProps<typeof Button>['size'];
disabled?: boolean;
}
>) {
return (
<Button
@ -37,6 +41,7 @@ export function DeleteButton({
onClick={() => handleClick()}
icon={Trash2}
className="!m-0"
data-cy={dataCy}
>
{children || 'Remove'}
</Button>

View File

@ -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<DockerConfig>;
onRemoveClick: (configs: Array<DockerConfig>) => void;
onRefresh: () => Promise<void>;
onRefresh: () => void;
}
const storageKey = 'docker_configs';
@ -54,24 +54,15 @@ export function ConfigsDatatable({ dataset, onRefresh, onRemoveClick }: Props) {
hasWriteAccessQuery.authorized && (
<div className="flex items-center gap-3">
<Authorized authorizations="DockerConfigDelete">
<Button
icon={Trash2}
color="dangerlight"
onClick={() => onRemoveClick(selectedRows)}
<DeleteButton
disabled={selectedRows.length === 0}
>
Remove
</Button>
onConfirmed={() => onRemoveClick(selectedRows)}
confirmMessage="Do you want to remove the selected config(s)?"
/>
</Authorized>
<Authorized authorizations="DockerConfigCreate">
<Button
icon={Plus}
as={Link}
props={{ to: 'docker.configs.new' }}
>
Add config
</Button>
<AddButton>Add config</AddButton>
</Authorized>
</div>
)

View File

@ -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({
</Authorized>
</ButtonGroup>
{isAddActionVisible && (
<Authorized authorizations="DockerContainerCreate">
<Link to="docker.containers.new" className="space-left">
<Button icon={Plus}>Add container</Button>
</Link>
</Authorized>
<div className="space-left">
<Authorized authorizations="DockerContainerCreate">
<AddButton>Add container</AddButton>
</Authorized>
</div>
)}
</div>
);

View File

@ -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({
/>
<Authorized authorizations="DockerImageBuild">
<Button
as={Link}
props={{ to: 'docker.images.build' }}
<AddButton
to="docker.images.build"
data-cy="image-buildImageButton"
icon={Plus}
>
Build a new image
</Button>
</AddButton>
</Authorized>
</div>
)}

View File

@ -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');
},
}
);
}
}

View File

@ -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({
<DetailsTable.Row label="Id">
{network.Id}
{allowRemoveNetwork && (
<Authorized authorizations="DockerNetworkDelete">
<Button
data-cy="networkDetails-deleteNetwork"
size="xsmall"
color="danger"
onClick={() => onRemoveNetworkClicked()}
>
<Icon
icon={Trash2}
className="space-right"
aria-hidden="true"
/>
Delete this network
</Button>
</Authorized>
<span className="ml-2">
<Authorized authorizations="DockerNetworkDelete">
<DeleteButton
data-cy="networkDetails-deleteNetwork"
size="xsmall"
onConfirmed={onRemoveNetworkClicked}
confirmMessage="Do you want to delete the network?"
>
Delete this network
</DeleteButton>
</Authorized>
</span>
)}
</DetailsTable.Row>
<DetailsTable.Row label="Driver">{network.Driver}</DetailsTable.Row>

View File

@ -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) {
<Authorized
authorizations={['DockerNetworkDelete', 'DockerNetworkCreate']}
>
<Button
<DeleteButton
disabled={selectedRows.length === 0}
color="dangerlight"
onClick={() => onRemove(selectedRows)}
icon={Trash2}
>
Remove
</Button>
confirmMessage="Do you want to remove the selected network(s)?"
onConfirmed={() => onRemove(selectedRows)}
/>
</Authorized>
<Authorized
authorizations="DockerNetworkCreate"
data-cy="network-addNetworkButton"
>
<Button icon={Plus} as={Link} props={{ to: '.new' }}>
<Authorized authorizations="DockerNetworkCreate">
<AddButton data-cy="network-addNetworkButton">
Add network
</Button>
</AddButton>
</Authorized>
</div>
)}

View File

@ -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 (
<div className="flex items-center gap-2">
<Authorized authorizations="DockerSecretDelete">
<Button
color="dangerlight"
<DeleteButton
disabled={selectedItems.length === 0}
onClick={() => onRemove(selectedItems)}
icon={Trash2}
className="!m-0"
onConfirmed={() => onRemove(selectedItems)}
confirmMessage="Do you want to remove the selected secret(s)?"
data-cy="secret-removeSecretButton"
>
Remove
</Button>
/>
</Authorized>
<Authorized authorizations="DockerSecretCreate">
<Button
as={Link}
props={{ to: '.new' }}
icon={Plus}
className="!m-0"
data-cy="secret-addSecretButton"
>
Add secret
</Button>
<AddButton data-cy="secret-addSecretButton">Add secret</AddButton>
</Authorized>
</div>
);

View File

@ -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({
</Authorized>
)}
<Authorized authorizations="DockerServiceDelete">
<Button
color="dangerlight"
<DeleteButton
disabled={selectedItems.length === 0}
onClick={() => handleRemove(selectedItems)}
icon={Trash2}
onConfirmed={() => handleRemove(selectedItems)}
confirmMessage="Do you want to remove the selected service(s)? All the containers associated to the selected service(s) will be removed too."
data-cy="service-removeServiceButton"
>
Remove
</Button>
/>
</Authorized>
</ButtonGroup>
{isAddActionVisible && (
<Authorized authorizations="DockerServiceCreate">
<Button
as={Link}
props={{ to: '.new' }}
icon={Plus}
className="!ml-0"
>
Add service
</Button>
<AddButton>Add service</AddButton>
</Authorized>
)}
</div>
@ -97,14 +86,6 @@ export function TableActions({
}
async function handleRemove(selectedItems: Array<ServiceViewModel>) {
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),
{

View File

@ -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 (
<div className="flex items-center gap-2">
<Authorized authorizations="PortainerStackDelete">
<Button
color="dangerlight"
<DeleteButton
disabled={selectedItems.length === 0}
onClick={() => onRemove(selectedItems)}
icon={Trash2}
className="!m-0"
onConfirmed={() => onRemove(selectedItems)}
confirmMessage="Do you want to remove the selected stack(s)? Associated services will be removed as well."
data-cy="stack-removeStackButton"
>
Remove
</Button>
/>
</Authorized>
<Authorized authorizations="PortainerStackCreate">
<Button
as={Link}
props={{ to: '.newstack' }}
icon={Plus}
className="!m-0"
data-cy="stack-addStackButton"
>
<AddButton data-cy="stack-addStackButton" to=".newstack">
Add stack
</Button>
</AddButton>
</Authorized>
</div>
);

View File

@ -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 (
<div className="flex items-center gap-2">
<Authorized authorizations="DockerVolumeDelete">
<Button
color="dangerlight"
<DeleteButton
disabled={selectedItems.length === 0}
onClick={() => onRemove(selectedItems)}
icon={Trash2}
className="!m-0"
onConfirmed={() => onRemove(selectedItems)}
confirmMessage="Do you want to remove the selected volume(s)?"
data-cy="volume-removeVolumeButton"
>
Remove
</Button>
/>
</Authorized>
<Authorized authorizations="DockerVolumeCreate">
<Button
as={Link}
props={{ to: '.new' }}
icon={Plus}
className="!m-0"
data-cy="volume-addVolumeButton"
>
Add volume
</Button>
<AddButton data-cy="volume-addVolumeButton">Add volume</AddButton>
</Authorized>
</div>
);

View File

@ -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 (
<>
<Button
onClick={() => handleRemoveDevice(selectedRows)}
<DeleteButton
onConfirmed={() => handleRemoveDevice(selectedRows)}
disabled={selectedRows.length === 0}
color="dangerlight"
icon={Trash2}
confirmMessage="You're about to remove edge device(s) from waiting room, which will not be shown until next agent startup."
>
Remove Device
</Button>
</DeleteButton>
<TooltipWithChildren
message={
@ -122,18 +120,6 @@ export function TableActions({
}
async function handleRemoveDevice(devices: Environment[]) {
const confirmed = await confirm({
title: 'Are you sure?',
message:
"You're about to remove edge device(s) from waiting room, which will not be shown until next agent startup.",
confirmButton: buildConfirmButton('Remove', 'danger'),
modalType: ModalType.Destructive,
});
if (!confirmed) {
return;
}
removeMutation.mutate(
devices.map((d) => d.Id),
{

View File

@ -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 (
<div className="flex items-center gap-2">
<Button
color="dangerlight"
<DeleteButton
disabled={selectedItems.length === 0}
onClick={() => handleRemove(selectedItems)}
icon={Trash2}
className="!m-0"
>
Remove
</Button>
onConfirmed={() => handleRemove(selectedItems)}
confirmMessage="Are you sure you want to remove the selected Edge stack(s)?"
/>
<Button
as={Link}
props={{ to: 'edge.stacks.new' }}
icon={Plus}
className="!m-0"
data-cy="edgeStack-addStackButton"
>
Add stack
</Button>
<AddButton data-cy="edgeStack-addStackButton">Add stack</AddButton>
</div>
);
async function handleRemove(selectedItems: Array<DecoratedEdgeStack>) {
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: () => {

View File

@ -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 && (
<Link
<AddButton
to="kubernetes.templates.custom.new"
data-cy="k8sAppDetail-createCustomTemplateButton"
params={{
fileContent: appStackFileQuery.data.StackFileContent,
}}
>
<Button
type="button"
color="primary"
size="small"
className="hover:decoration-none !ml-0"
data-cy="k8sAppDetail-createCustomTemplateButton"
>
<Icon icon={Plus} className="mr-1" />
Create template from application
</Button>
</Link>
Create template from application
</AddButton>
)}
</div>
)}

View File

@ -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 (
<Authorized authorizations="K8sApplicationsW">
<Button
<DeleteButton
confirmMessage="Are you sure that you want to remove the selected stack(s) ? This will remove all the applications associated to the stack(s)."
disabled={selectedItems.length === 0}
color="dangerlight"
onClick={() => onRemove(selectedItems)}
icon={Trash2}
onConfirmed={() => onRemove(selectedItems)}
data-cy="k8sApp-removeStackButton"
>
Remove
</Button>
/>
</Authorized>
);
}

View File

@ -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 (
<Authorized authorizations="K8sConfigMapsW">
<Button
className="btn-wrapper"
color="dangerlight"
<DeleteButton
disabled={selectedItems.length === 0}
onClick={async () => {
handleRemoveClick(selectedItems);
}}
icon={Trash2}
onConfirmed={() => handleRemoveClick(selectedItems)}
confirmMessage={`Are you sure you want to remove the selected ${pluralize(
selectedItems.length,
'ConfigMap'
)}`}
data-cy="k8sConfig-removeConfigButton"
/>
<AddButton
to="kubernetes.configmaps.new"
data-cy="k8sConfig-addConfigWithFormButton"
color="secondary"
>
Remove
</Button>
<Link to="kubernetes.configmaps.new" className="ml-1">
<Button
className="btn-wrapper"
color="secondary"
icon={Plus}
data-cy="k8sConfig-addConfigWithFormButton"
>
Add with form
</Button>
</Link>
<Link
to="kubernetes.deploy"
Add with form
</AddButton>
<CreateFromManifestButton
params={{
referrer: 'kubernetes.configurations',
tab: 'configmaps',
}}
className="ml-1"
data-cy="k8sConfig-deployFromManifestButton"
>
<Button className="btn-wrapper" color="primary" icon={Plus}>
Create from manifest
</Button>
</Link>
/>
</Authorized>
);
}

View File

@ -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 (
<Authorized authorizations="K8sSecretsW">
<Button
className="btn-wrapper"
color="dangerlight"
<DeleteButton
disabled={selectedItems.length === 0}
onClick={async () => {
handleRemoveClick(selectedItems);
}}
icon={Trash2}
onConfirmed={() => handleRemoveClick(selectedItems)}
data-cy="k8sSecret-removeSecretButton"
confirmMessage={`Are you sure you want to remove the selected ${pluralize(
selectedItems.length,
'secret'
)}?`}
/>
<AddButton
to="kubernetes.secrets.new"
data-cy="k8sSecret-addSecretWithFormButton"
color="secondary"
>
Remove
</Button>
<Link to="kubernetes.secrets.new" className="ml-1">
<Button
className="btn-wrapper"
color="secondary"
icon={Plus}
data-cy="k8sSecret-addSecretWithFormButton"
>
Add with form
</Button>
</Link>
<Link
to="kubernetes.deploy"
Add with form
</AddButton>
<CreateFromManifestButton
params={{
referrer: 'kubernetes.configurations',
tab: 'secrets',
}}
className="ml-1"
data-cy="k8sSecret-deployFromManifestButton"
>
<Button className="btn-wrapper" color="primary" icon={Plus}>
Create from manifest
</Button>
</Link>
/>
</Authorized>
);
}

View File

@ -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 (
<div className="ingressDatatable-actions">
<Authorized authorizations="AzureContainerGroupDelete">
<Button
color="dangerlight"
disabled={selectedFlatRows.length === 0}
onClick={() => handleRemoveClick(selectedFlatRows)}
icon={Trash2}
>
Remove
</Button>
</Authorized>
<Authorized authorizations="K8sIngressesW">
<DeleteButton
disabled={selectedFlatRows.length === 0}
onConfirmed={() => handleRemoveClick(selectedFlatRows)}
data-cy="k8sSecret-removeSecretButton"
confirmMessage="Are you sure you want to delete the selected ingresses?"
/>
<Authorized authorizations="K8sIngressesW">
<Link
to="kubernetes.ingresses.create"
className="space-left no-decoration"
>
<Button icon={Plus} color="secondary">
Add with form
</Button>
</Link>
</Authorized>
<Authorized authorizations="K8sIngressesW">
<Link
to="kubernetes.deploy"
className="space-left no-decoration"
params={{ referrer: 'kubernetes.ingresses' }}
>
<Button icon={Plus}>Create from manifest</Button>
</Link>
</Authorized>
</div>
<AddButton to=".create" color="secondary">
Add with form
</AddButton>
<CreateFromManifestButton />
</Authorized>
);
}
@ -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;
}
}

View File

@ -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 {

View File

@ -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(
<>
<p>{`Are you sure you want to remove the selected ${pluralize(
services.length,
'service'
)}?`}</p>
<ul className="pl-6">
{services.map((s, index) => (
<li key={index}>
{s.Namespace}/{s.Name}
</li>
))}
</ul>
</>
);
if (!confirmed) {
return null;
}
return (
<Authorized authorizations="K8sServicesW">
<DeleteButton
disabled={selectedItems.length === 0}
onConfirmed={() => handleRemoveClick(selectedItems)}
confirmMessage={
<>
<p>{`Are you sure you want to remove the selected ${pluralize(
selectedItems.length,
'service'
)}?`}</p>
<ul className="pl-6">
{selectedItems.map((s, index) => (
<li key={index}>
{s.Namespace}/{s.Name}
</li>
))}
</ul>
</>
}
/>
<CreateFromManifestButton />
</Authorized>
);
async function handleRemoveClick(services: SelectedService[]) {
const payload: Record<string, string[]> = {};
services.forEach((service) => {
payload[service.Namespace] = payload[service.Namespace] || [];
@ -181,32 +187,5 @@ function TableActions({ selectedItems }: TableActionsProps) {
},
}
);
return services;
}
return (
<div className="servicesDatatable-actions">
<Authorized authorizations="K8sServicesW">
<Button
className="btn-wrapper"
color="dangerlight"
disabled={selectedItems.length === 0}
onClick={() => handleRemoveClick(selectedItems)}
icon={Trash2}
>
Remove
</Button>
<Link
to="kubernetes.deploy"
params={{ referrer: 'kubernetes.services' }}
className="space-left hover:no-decoration"
>
<Button className="btn-wrapper" color="primary" icon="plus">
Create from manifest
</Button>
</Link>
</Authorized>
</div>
);
}

View File

@ -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 (
<div className="flex gap-2">
<Button
disabled={selectedItems.length < 1}
color="dangerlight"
onClick={() => onDeleteClick(selectedItems)}
<>
<DeleteButton
disabled={selectedItems.length === 0}
onConfirmed={() => onDeleteClick(selectedItems)}
confirmMessage={`Are you sure you want to remove the selected Helm ${pluralize(
selectedItems.length,
'repository',
'repositories'
)}?`}
data-cy="credentials-deleteButton"
icon={Trash2}
/>
<AddButton
to="portainer.account.createHelmRepository"
data-cy="credentials-addButton"
>
Remove
</Button>
<AddButton to="portainer.account.createHelmRepository">
Add Helm repository
</AddButton>
</div>
</>
);
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();

View File

@ -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({
<ImportFdoDeviceButton />
{isBE && (
<Button
as={Link}
<AddButton
color="secondary"
icon={Plus}
props={{ to: 'portainer.endpoints.edgeAutoCreateScript' }}
to="portainer.endpoints.edgeAutoCreateScript"
>
Auto onboarding
</Button>
</AddButton>
)}
<Link to="portainer.wizard.endpoints">
<Button
onClick={() =>
localStorage.setItem('wizardReferrer', 'environments')
}
icon={Plus}
className="!m-0"
>
Add environment
</Button>
</Link>
<AddButton
to="portainer.wizard.endpoints"
params={{ referrer: 'environments' }}
>
Add environment
</AddButton>
</div>
)}
/>

View File

@ -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 (
<Button
type="button"
color="secondary"
icon={Plus}
as={Link}
props={{ to: 'portainer.endpoints.importDevice' }}
className="ml-[5px]"
>
Import FDO device
</Button>
<div className="ml-[5px]">
<AddButton color="secondary" to="portainer.endpoints.importDevice">
Import FDO device
</AddButton>
</div>
);
}

View File

@ -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 (
<>
<Button
icon={Trash2}
color="dangerlight"
onClick={() => handleRemove()}
<DeleteButton
onConfirmed={() => handleRemove()}
disabled={selectedRows.length === 0}
>
Remove
</Button>
<Link to=".create">
<Button>Add update & rollback schedule</Button>
</Link>
confirmMessage="Are you sure you want to remove these schedules?"
/>
<AddButton to=".create">Add update & rollback schedule</AddButton>
</>
);
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');

View File

@ -33,7 +33,7 @@ import { WizardEndpointsList } from './WizardEndpointsList';
export function EnvironmentCreationView() {
const {
params: { localEndpointId: localEndpointIdParam },
params: { localEndpointId: localEndpointIdParam, referrer },
} = useCurrentStateAndParams();
const [environmentIds, setEnvironmentIds] = useState<EnvironmentId[]>(() => {
@ -130,8 +130,7 @@ export function EnvironmentCreationView() {
])
),
});
if (localStorage.getItem('wizardReferrer') === 'environments') {
localStorage.removeItem('wizardReferrer');
if (referrer === 'environments') {
router.stateService.go('portainer.endpoints');
return;
}

View File

@ -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 (
<Button
icon={Trash2}
color="dangerlight"
onClick={() => handleRemove()}
<DeleteButton
onConfirmed={() => handleRemove()}
disabled={selectedRows.length === 0}
>
Remove
</Button>
confirmMessage="Are you sure you want to remove the selected notifications?"
/>
);
function handleRemove() {

View File

@ -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 (
<div className="actionBar">
<>
<Link to="portainer.endpoints.profile" className="space-left">
<Button disabled={!isFDOEnabled} icon={PlusCircle}>
Add Profile
@ -42,15 +42,12 @@ export function FDOProfilesDatatableActions({
Duplicate
</Button>
<Button
disabled={!isFDOEnabled || selectedItems.length < 1}
color="danger"
onClick={() => onDeleteProfileClick()}
icon={Trash2}
>
Remove
</Button>
</div>
<DeleteButton
disabled={!isFDOEnabled || selectedItems.length === 0}
onConfirmed={() => 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 {

View File

@ -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(
<AppTemplatesListItem
template={template}
onSelect={onSelect}
isSelected={isSelected}
/>
);
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<object>) => ({
...(await importOriginal()),
UISref: ({ children }: PropsWithChildren<unknown>) => 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(
<AppTemplatesListItem
template={
{
Type: type,
} as TemplateViewModel
}
onSelect={onSelect}
isSelected={isSelected}
/>
);
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(
<AppTemplatesListItem
template={template}
onSelect={onSelect}
isSelected={isSelected}
/>
);
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(
<AppTemplatesListItem
template={template}
onSelect={onSelect}
isSelected={isSelected}
/>
);
}

View File

@ -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={() => (
<Button as={Link} props={{ to: '.new' }} icon={Plus}>
Add Custom Template
</Button>
)}
renderTableActions={() => <AddButton>Add Custom Template</AddButton>}
/>
<div className="blocklist gap-y-2 !px-[20px] !pb-[20px]" role="list">

View File

@ -46,7 +46,6 @@ export function CustomTemplatesListItem({
<Button
as={Link}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
color="secondary"

View File

@ -1,6 +1,6 @@
import { useRouter } from '@uirouter/react';
import { useMutation, useQueryClient } from 'react-query';
import { Trash2, Users } from 'lucide-react';
import { Users } from 'lucide-react';
import { usePublicSettings } from '@/react/portainer/settings/queries';
import {
@ -9,9 +9,8 @@ import {
withInvalidate,
} from '@/react-tools/react-query';
import { confirmDelete } from '@@/modals/confirm';
import { Button } from '@@/buttons';
import { Widget } from '@@/Widget';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { Team, TeamId, TeamMembership, TeamRole } from '../types';
import { deleteTeam } from '../teams.service';
@ -45,17 +44,18 @@ export function Details({ team, memberships, isAdmin }: Props) {
<tr>
<td>Name</td>
<td>
{!teamSyncQuery.data && team.Name}
{isAdmin && (
<Button
color="danger"
size="xsmall"
onClick={handleDeleteClick}
icon={Trash2}
>
Delete this team
</Button>
)}
<div className="flex gap-2">
{!teamSyncQuery.data && team.Name}
{isAdmin && (
<DeleteButton
size="xsmall"
onConfirmed={handleDeleteClick}
confirmMessage="Do you want to delete this team? Users in this team will not be deleted."
>
Delete this team
</DeleteButton>
)}
</div>
</td>
</tr>
<tr>
@ -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);
}
}

View File

@ -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 && (
<Button
color="dangerlight"
onClick={() => handleRemoveClick(selectedRows)}
<DeleteButton
onConfirmed={() => handleRemoveClick(selectedRows)}
disabled={selectedRows.length === 0}
icon={Trash2}
>
Remove
</Button>
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', '');