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 angular from 'angular';
import { confirmDelete } from '@@/modals/confirm';
class ConfigsController { class ConfigsController {
/* @ngInject */ /* @ngInject */
@ -34,10 +33,6 @@ class ConfigsController {
} }
async removeAction(selectedItems) { 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); return this.$async(this.removeActionAsync, selectedItems);
} }

View File

@ -1,6 +1,5 @@
import _ from 'lodash-es'; import _ from 'lodash-es';
import DockerNetworkHelper from '@/docker/helpers/networkHelper'; import DockerNetworkHelper from '@/docker/helpers/networkHelper';
import { confirmDelete } from '@@/modals/confirm';
angular.module('portainer.docker').controller('NetworksController', [ angular.module('portainer.docker').controller('NetworksController', [
'$q', '$q',
@ -13,10 +12,6 @@ angular.module('portainer.docker').controller('NetworksController', [
'AgentService', 'AgentService',
function ($q, $scope, $state, NetworkService, Notifications, HttpRequestHelper, endpoint, AgentService) { function ($q, $scope, $state, NetworkService, Notifications, HttpRequestHelper, endpoint, AgentService) {
$scope.removeAction = async function (selectedItems) { $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; var actionCount = selectedItems.length;
angular.forEach(selectedItems, function (network) { angular.forEach(selectedItems, function (network) {
HttpRequestHelper.setPortainerAgentTargetHeader(network.NodeName); HttpRequestHelper.setPortainerAgentTargetHeader(network.NodeName);

View File

@ -1,4 +1,3 @@
import { confirmDelete } from '@@/modals/confirm';
angular.module('portainer.docker').controller('SecretsController', [ angular.module('portainer.docker').controller('SecretsController', [
'$scope', '$scope',
'$state', '$state',
@ -6,10 +5,6 @@ angular.module('portainer.docker').controller('SecretsController', [
'Notifications', 'Notifications',
function ($scope, $state, SecretService, Notifications) { function ($scope, $state, SecretService, Notifications) {
$scope.removeAction = async function (selectedItems) { $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; var actionCount = selectedItems.length;
angular.forEach(selectedItems, function (secret) { angular.forEach(selectedItems, function (secret) {
SecretService.remove(secret.Id) SecretService.remove(secret.Id)

View File

@ -1,5 +1,3 @@
import { confirmDelete } from '@@/modals/confirm';
angular.module('portainer.docker').controller('VolumesController', [ angular.module('portainer.docker').controller('VolumesController', [
'$q', '$q',
'$scope', '$scope',
@ -13,28 +11,24 @@ angular.module('portainer.docker').controller('VolumesController', [
'endpoint', 'endpoint',
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, Authentication, endpoint) { function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, Authentication, endpoint) {
$scope.removeAction = function (selectedItems) { $scope.removeAction = function (selectedItems) {
confirmDelete('Do you want to remove the selected volume(s)?').then((confirmed) => { var actionCount = selectedItems.length;
if (confirmed) { angular.forEach(selectedItems, function (volume) {
var actionCount = selectedItems.length; HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName);
angular.forEach(selectedItems, function (volume) { VolumeService.remove(volume)
HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName); .then(function success() {
VolumeService.remove(volume) Notifications.success('Volume successfully removed', volume.Id);
.then(function success() { var index = $scope.volumes.indexOf(volume);
Notifications.success('Volume successfully removed', volume.Id); $scope.volumes.splice(index, 1);
var index = $scope.volumes.indexOf(volume); })
$scope.volumes.splice(index, 1); .catch(function error(err) {
}) Notifications.error('Failure', err, 'Unable to remove volume');
.catch(function error(err) { })
Notifications.error('Failure', err, 'Unable to remove volume'); .finally(function final() {
}) --actionCount;
.finally(function final() { if (actionCount === 0) {
--actionCount; $state.reload();
if (actionCount === 0) { }
$state.reload();
}
});
}); });
}
}); });
}; };

View File

@ -79,11 +79,7 @@ class KubernetesApplicationsController {
} }
removeStacksAction(selectedItems) { 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) => { return this.$async(this.removeStacksActionAsync, selectedItems);
if (confirmed) {
return this.$async(this.removeStacksActionAsync, selectedItems);
}
});
} }
async removeActionAsync(selectedItems) { async removeActionAsync(selectedItems) {

View File

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

View File

@ -1,16 +1,9 @@
import { confirmDelete } from '@@/modals/confirm';
angular.module('portainer.app').controller('StacksController', StacksController); angular.module('portainer.app').controller('StacksController', StacksController);
/* @ngInject */ /* @ngInject */
function StacksController($scope, $state, Notifications, StackService, Authentication, endpoint) { function StacksController($scope, $state, Notifications, StackService, Authentication, endpoint) {
$scope.removeAction = function (selectedItems) { $scope.removeAction = function (selectedItems) {
confirmDelete('Do you want to remove the selected stack(s)? Associated services will be removed as well.').then((confirmed) => { return deleteSelectedStacks(selectedItems);
if (!confirmed) {
return;
}
deleteSelectedStacks(selectedItems);
});
}; };
function deleteSelectedStacks(stacks) { 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 { ContainerGroup } from '@/react/azure/types';
import { Authorized } from '@/react/hooks/useUser'; import { Authorized } from '@/react/hooks/useUser';
import { confirmDelete } from '@@/modals/confirm';
import { Datatable } from '@@/datatables'; import { Datatable } from '@@/datatables';
import { Button } from '@@/buttons'; import { AddButton } from '@@/buttons';
import { Link } from '@@/Link';
import { createPersistedStore } from '@@/datatables/types'; import { createPersistedStore } from '@@/datatables/types';
import { useTableState } from '@@/datatables/useTableState'; import { useTableState } from '@@/datatables/useTableState';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { columns } from './columns'; import { columns } from './columns';
@ -33,36 +32,26 @@ export function ContainersDatatable({ dataset, onRemoveClick }: Props) {
getRowId={(container) => container.id} getRowId={(container) => container.id}
emptyContentLabel="No container available." emptyContentLabel="No container available."
renderTableActions={(selectedRows) => ( renderTableActions={(selectedRows) => (
<> <div className="flex gap-2">
<Authorized authorizations="AzureContainerGroupDelete"> <Authorized authorizations="AzureContainerGroupDelete">
<Button <DeleteButton
color="dangerlight"
disabled={selectedRows.length === 0} disabled={selectedRows.length === 0}
onClick={() => handleRemoveClick(selectedRows.map((r) => r.id))} onConfirmed={() =>
icon={Trash2} handleRemoveClick(selectedRows.map((r) => r.id))
> }
Remove confirmMessage="Are you sure you want to delete the selected containers?"
</Button> />
</Authorized> </Authorized>
<Authorized authorizations="AzureContainerGroupCreate"> <Authorized authorizations="AzureContainerGroupCreate">
<Link to="azure.containerinstances.new" className="space-left"> <AddButton>Add container</AddButton>
<Button icon={Plus}>Add container</Button>
</Link>
</Authorized> </Authorized>
</> </div>
)} )}
/> />
); );
async function handleRemoveClick(containerIds: string[]) { 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); return onRemoveClick(containerIds);
} }
} }

View File

@ -1,5 +1,5 @@
import { PropsWithChildren, AnchorHTMLAttributes } from 'react'; import { PropsWithChildren, AnchorHTMLAttributes } from 'react';
import { UISref, UISrefProps } from '@uirouter/react'; import { UISrefProps, useSref } from '@uirouter/react';
interface Props { interface Props {
title?: string; title?: string;
@ -8,18 +8,18 @@ interface Props {
} }
export function Link({ export function Link({
title = '',
className,
children, children,
to,
params,
options,
...props ...props
}: PropsWithChildren<Props> & UISrefProps) { }: PropsWithChildren<Props> & UISrefProps) {
const { onClick, href } = useSref(to, params, options);
return ( return (
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
<UISref className={className} {...props}> <a onClick={onClick} href={href} {...props}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} {children}
<a title={title} target={props.target} rel={props.rel}> </a>
{children}
</a>
</UISref>
); );
} }

View File

@ -1,6 +1,8 @@
import { Trash2 } from 'lucide-react'; import { Trash2 } from 'lucide-react';
import { ComponentProps, PropsWithChildren, ReactNode } from 'react'; import { ComponentProps, PropsWithChildren, ReactNode } from 'react';
import { AutomationTestingProps } from '@/types';
import { confirmDelete } from '@@/modals/confirm'; import { confirmDelete } from '@@/modals/confirm';
import { Button } from './Button'; import { Button } from './Button';
@ -21,13 +23,15 @@ type ConfirmOrClick =
export function DeleteButton({ export function DeleteButton({
disabled, disabled,
size, size,
'data-cy': dataCy,
children, children,
...props ...props
}: PropsWithChildren< }: PropsWithChildren<
ConfirmOrClick & { AutomationTestingProps &
size?: ComponentProps<typeof Button>['size']; ConfirmOrClick & {
disabled?: boolean; size?: ComponentProps<typeof Button>['size'];
} disabled?: boolean;
}
>) { >) {
return ( return (
<Button <Button
@ -37,6 +41,7 @@ export function DeleteButton({
onClick={() => handleClick()} onClick={() => handleClick()}
icon={Trash2} icon={Trash2}
className="!m-0" className="!m-0"
data-cy={dataCy}
> >
{children || 'Remove'} {children || 'Remove'}
</Button> </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 { Authorized, useAuthorizations } from '@/react/hooks/useUser';
import { Datatable, TableSettingsMenu } from '@@/datatables'; import { Datatable, TableSettingsMenu } from '@@/datatables';
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh'; import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
import { useRepeater } from '@@/datatables/useRepeater'; import { useRepeater } from '@@/datatables/useRepeater';
import { Button } from '@@/buttons'; import { AddButton } from '@@/buttons';
import { Link } from '@@/Link';
import { useTableState } from '@@/datatables/useTableState'; import { useTableState } from '@@/datatables/useTableState';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { DockerConfig } from '../../types'; import { DockerConfig } from '../../types';
@ -17,7 +17,7 @@ import { createStore } from './store';
interface Props { interface Props {
dataset: Array<DockerConfig>; dataset: Array<DockerConfig>;
onRemoveClick: (configs: Array<DockerConfig>) => void; onRemoveClick: (configs: Array<DockerConfig>) => void;
onRefresh: () => Promise<void>; onRefresh: () => void;
} }
const storageKey = 'docker_configs'; const storageKey = 'docker_configs';
@ -54,24 +54,15 @@ export function ConfigsDatatable({ dataset, onRefresh, onRemoveClick }: Props) {
hasWriteAccessQuery.authorized && ( hasWriteAccessQuery.authorized && (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Authorized authorizations="DockerConfigDelete"> <Authorized authorizations="DockerConfigDelete">
<Button <DeleteButton
icon={Trash2}
color="dangerlight"
onClick={() => onRemoveClick(selectedRows)}
disabled={selectedRows.length === 0} disabled={selectedRows.length === 0}
> onConfirmed={() => onRemoveClick(selectedRows)}
Remove confirmMessage="Do you want to remove the selected config(s)?"
</Button> />
</Authorized> </Authorized>
<Authorized authorizations="DockerConfigCreate"> <Authorized authorizations="DockerConfigCreate">
<Button <AddButton>Add config</AddButton>
icon={Plus}
as={Link}
props={{ to: 'docker.configs.new' }}
>
Add config
</Button>
</Authorized> </Authorized>
</div> </div>
) )

View File

@ -1,13 +1,5 @@
import { useRouter } from '@uirouter/react'; import { useRouter } from '@uirouter/react';
import { import { Pause, Play, RefreshCw, Slash, Square, Trash2 } from 'lucide-react';
Pause,
Play,
Plus,
RefreshCw,
Slash,
Square,
Trash2,
} from 'lucide-react';
import * as notifications from '@/portainer/services/notifications'; import * as notifications from '@/portainer/services/notifications';
import { useAuthorizations, Authorized } from '@/react/hooks/useUser'; import { useAuthorizations, Authorized } from '@/react/hooks/useUser';
@ -29,8 +21,7 @@ import {
} from '@/react/docker/containers/containers.service'; } from '@/react/docker/containers/containers.service';
import type { EnvironmentId } from '@/react/portainer/environments/types'; import type { EnvironmentId } from '@/react/portainer/environments/types';
import { Link } from '@@/Link'; import { ButtonGroup, Button, AddButton } from '@@/buttons';
import { ButtonGroup, Button } from '@@/buttons';
type ContainerServiceAction = ( type ContainerServiceAction = (
endpointId: EnvironmentId, endpointId: EnvironmentId,
@ -166,11 +157,11 @@ export function ContainersDatatableActions({
</Authorized> </Authorized>
</ButtonGroup> </ButtonGroup>
{isAddActionVisible && ( {isAddActionVisible && (
<Authorized authorizations="DockerContainerCreate"> <div className="space-left">
<Link to="docker.containers.new" className="space-left"> <Authorized authorizations="DockerContainerCreate">
<Button icon={Plus}>Add container</Button> <AddButton>Add container</AddButton>
</Link> </Authorized>
</Authorized> </div>
)} )}
</div> </div>
); );

View File

@ -1,11 +1,4 @@
import { import { ChevronDown, Download, List, Trash2, Upload } from 'lucide-react';
ChevronDown,
Download,
List,
Plus,
Trash2,
Upload,
} from 'lucide-react';
import { Menu, MenuButton, MenuItem, MenuPopover } from '@reach/menu-button'; import { Menu, MenuButton, MenuItem, MenuPopover } from '@reach/menu-button';
import { positionRight } from '@reach/popover'; import { positionRight } from '@reach/popover';
import { useMemo } from 'react'; import { useMemo } from 'react';
@ -21,7 +14,7 @@ import {
RefreshableTableSettings, RefreshableTableSettings,
} from '@@/datatables/types'; } from '@@/datatables/types';
import { useTableState } from '@@/datatables/useTableState'; import { useTableState } from '@@/datatables/useTableState';
import { Button, ButtonGroup, LoadingButton } from '@@/buttons'; import { AddButton, Button, ButtonGroup, LoadingButton } from '@@/buttons';
import { Link } from '@@/Link'; import { Link } from '@@/Link';
import { ButtonWithRef } from '@@/buttons/Button'; import { ButtonWithRef } from '@@/buttons/Button';
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh'; import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
@ -82,14 +75,12 @@ export function ImagesDatatable({
/> />
<Authorized authorizations="DockerImageBuild"> <Authorized authorizations="DockerImageBuild">
<Button <AddButton
as={Link} to="docker.images.build"
props={{ to: 'docker.images.build' }}
data-cy="image-buildImageButton" data-cy="image-buildImageButton"
icon={Plus}
> >
Build a new image Build a new image
</Button> </AddButton>
</Authorized> </Authorized>
</div> </div>
)} )}

View File

@ -8,7 +8,6 @@ import { DockerContainer } from '@/react/docker/containers/types';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel'; import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
import { useContainers } from '@/react/docker/containers/queries/containers'; import { useContainers } from '@/react/docker/containers/queries/containers';
import { confirmDelete } from '@@/modals/confirm';
import { PageHeader } from '@@/PageHeader'; import { PageHeader } from '@@/PageHeader';
import { useNetwork, useDeleteNetwork } from '../queries'; import { useNetwork, useDeleteNetwork } from '../queries';
@ -95,19 +94,14 @@ export function ItemView() {
); );
async function onRemoveNetworkClicked() { async function onRemoveNetworkClicked() {
const message = 'Do you want to delete the network?'; deleteNetworkMutation.mutate(
const confirmed = await confirmDelete(message); { environmentId, networkId },
{
if (confirmed) { onSuccess: () => {
deleteNetworkMutation.mutate( router.stateService.go('docker.networks');
{ environmentId, networkId }, },
{ }
onSuccess: () => { );
router.stateService.go('docker.networks');
},
}
);
}
} }
} }

View File

@ -1,13 +1,12 @@
import { Fragment } from 'react'; import { Fragment } from 'react';
import { Network, Trash2 } from 'lucide-react'; import { Network } from 'lucide-react';
import DockerNetworkHelper from '@/docker/helpers/networkHelper'; import DockerNetworkHelper from '@/docker/helpers/networkHelper';
import { Authorized } from '@/react/hooks/useUser'; import { Authorized } from '@/react/hooks/useUser';
import { TableContainer, TableTitle } from '@@/datatables'; import { TableContainer, TableTitle } from '@@/datatables';
import { DetailsTable } from '@@/DetailsTable'; import { DetailsTable } from '@@/DetailsTable';
import { Button } from '@@/buttons'; import { DeleteButton } from '@@/buttons/DeleteButton';
import { Icon } from '@@/Icon';
import { isSystemNetwork } from '../network.helper'; import { isSystemNetwork } from '../network.helper';
import { DockerNetwork, IPConfig } from '../types'; import { DockerNetwork, IPConfig } from '../types';
@ -38,21 +37,18 @@ export function NetworkDetailsTable({
<DetailsTable.Row label="Id"> <DetailsTable.Row label="Id">
{network.Id} {network.Id}
{allowRemoveNetwork && ( {allowRemoveNetwork && (
<Authorized authorizations="DockerNetworkDelete"> <span className="ml-2">
<Button <Authorized authorizations="DockerNetworkDelete">
data-cy="networkDetails-deleteNetwork" <DeleteButton
size="xsmall" data-cy="networkDetails-deleteNetwork"
color="danger" size="xsmall"
onClick={() => onRemoveNetworkClicked()} onConfirmed={onRemoveNetworkClicked}
> confirmMessage="Do you want to delete the network?"
<Icon >
icon={Trash2} Delete this network
className="space-right" </DeleteButton>
aria-hidden="true" </Authorized>
/> </span>
Delete this network
</Button>
</Authorized>
)} )}
</DetailsTable.Row> </DetailsTable.Row>
<DetailsTable.Row label="Driver">{network.Driver}</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 { Authorized } from '@/react/hooks/useUser';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
@ -10,12 +10,12 @@ import {
refreshableSettings, refreshableSettings,
RefreshableTableSettings, RefreshableTableSettings,
} from '@@/datatables/types'; } from '@@/datatables/types';
import { Button } from '@@/buttons'; import { AddButton } from '@@/buttons';
import { TableSettingsMenu } from '@@/datatables'; import { TableSettingsMenu } from '@@/datatables';
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh'; import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
import { useRepeater } from '@@/datatables/useRepeater'; import { useRepeater } from '@@/datatables/useRepeater';
import { useTableState } from '@@/datatables/useTableState'; import { useTableState } from '@@/datatables/useTableState';
import { Link } from '@@/Link'; import { DeleteButton } from '@@/buttons/DeleteButton';
import { useIsSwarm } from '../../proxy/queries/useInfo'; import { useIsSwarm } from '../../proxy/queries/useInfo';
@ -80,22 +80,16 @@ export function NetworksDatatable({ dataset, onRemove, onRefresh }: Props) {
<Authorized <Authorized
authorizations={['DockerNetworkDelete', 'DockerNetworkCreate']} authorizations={['DockerNetworkDelete', 'DockerNetworkCreate']}
> >
<Button <DeleteButton
disabled={selectedRows.length === 0} disabled={selectedRows.length === 0}
color="dangerlight" confirmMessage="Do you want to remove the selected network(s)?"
onClick={() => onRemove(selectedRows)} onConfirmed={() => onRemove(selectedRows)}
icon={Trash2} />
>
Remove
</Button>
</Authorized> </Authorized>
<Authorized <Authorized authorizations="DockerNetworkCreate">
authorizations="DockerNetworkCreate" <AddButton data-cy="network-addNetworkButton">
data-cy="network-addNetworkButton"
>
<Button icon={Plus} as={Link} props={{ to: '.new' }}>
Add network Add network
</Button> </AddButton>
</Authorized> </Authorized>
</div> </div>
)} )}

View File

@ -1,5 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table'; 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 { SecretViewModel } from '@/docker/models/secret';
import { isoDate } from '@/portainer/filters/filters'; import { isoDate } from '@/portainer/filters/filters';
@ -15,9 +15,9 @@ import {
} from '@@/datatables/types'; } from '@@/datatables/types';
import { useTableState } from '@@/datatables/useTableState'; import { useTableState } from '@@/datatables/useTableState';
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh'; import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
import { Button } from '@@/buttons'; import { AddButton } from '@@/buttons';
import { Link } from '@@/Link';
import { useRepeater } from '@@/datatables/useRepeater'; import { useRepeater } from '@@/datatables/useRepeater';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { createOwnershipColumn } from '../../components/datatable/createOwnershipColumn'; import { createOwnershipColumn } from '../../components/datatable/createOwnershipColumn';
@ -96,28 +96,16 @@ function TableActions({
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Authorized authorizations="DockerSecretDelete"> <Authorized authorizations="DockerSecretDelete">
<Button <DeleteButton
color="dangerlight"
disabled={selectedItems.length === 0} disabled={selectedItems.length === 0}
onClick={() => onRemove(selectedItems)} onConfirmed={() => onRemove(selectedItems)}
icon={Trash2} confirmMessage="Do you want to remove the selected secret(s)?"
className="!m-0"
data-cy="secret-removeSecretButton" data-cy="secret-removeSecretButton"
> />
Remove
</Button>
</Authorized> </Authorized>
<Authorized authorizations="DockerSecretCreate"> <Authorized authorizations="DockerSecretCreate">
<Button <AddButton data-cy="secret-addSecretButton">Add secret</AddButton>
as={Link}
props={{ to: '.new' }}
icon={Plus}
className="!m-0"
data-cy="secret-addSecretButton"
>
Add secret
</Button>
</Authorized> </Authorized>
</div> </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 { useRouter } from '@uirouter/react';
import { ServiceViewModel } from '@/docker/models/service'; import { ServiceViewModel } from '@/docker/models/service';
@ -6,9 +6,8 @@ import { Authorized } from '@/react/hooks/useUser';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { notifySuccess } from '@/portainer/services/notifications'; import { notifySuccess } from '@/portainer/services/notifications';
import { Link } from '@@/Link'; import { AddButton, Button, ButtonGroup } from '@@/buttons';
import { Button, ButtonGroup } from '@@/buttons'; import { DeleteButton } from '@@/buttons/DeleteButton';
import { confirmDelete } from '@@/modals/confirm';
import { confirmServiceForceUpdate } from '../../common/update-service-modal'; import { confirmServiceForceUpdate } from '../../common/update-service-modal';
@ -46,28 +45,18 @@ export function TableActions({
</Authorized> </Authorized>
)} )}
<Authorized authorizations="DockerServiceDelete"> <Authorized authorizations="DockerServiceDelete">
<Button <DeleteButton
color="dangerlight"
disabled={selectedItems.length === 0} disabled={selectedItems.length === 0}
onClick={() => handleRemove(selectedItems)} onConfirmed={() => handleRemove(selectedItems)}
icon={Trash2} 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" data-cy="service-removeServiceButton"
> />
Remove
</Button>
</Authorized> </Authorized>
</ButtonGroup> </ButtonGroup>
{isAddActionVisible && ( {isAddActionVisible && (
<Authorized authorizations="DockerServiceCreate"> <Authorized authorizations="DockerServiceCreate">
<Button <AddButton>Add service</AddButton>
as={Link}
props={{ to: '.new' }}
icon={Plus}
className="!ml-0"
>
Add service
</Button>
</Authorized> </Authorized>
)} )}
</div> </div>
@ -97,14 +86,6 @@ export function TableActions({
} }
async function handleRemove(selectedItems: Array<ServiceViewModel>) { 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( removeMutation.mutate(
selectedItems.map((service) => service.Id), selectedItems.map((service) => service.Id),
{ {

View File

@ -1,9 +1,7 @@
import { Trash2, Plus } from 'lucide-react';
import { Authorized } from '@/react/hooks/useUser'; import { Authorized } from '@/react/hooks/useUser';
import { Link } from '@@/Link'; import { AddButton } from '@@/buttons';
import { Button } from '@@/buttons'; import { DeleteButton } from '@@/buttons/DeleteButton';
import { DecoratedStack } from './types'; import { DecoratedStack } from './types';
@ -17,28 +15,18 @@ export function TableActions({
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Authorized authorizations="PortainerStackDelete"> <Authorized authorizations="PortainerStackDelete">
<Button <DeleteButton
color="dangerlight"
disabled={selectedItems.length === 0} disabled={selectedItems.length === 0}
onClick={() => onRemove(selectedItems)} onConfirmed={() => onRemove(selectedItems)}
icon={Trash2} confirmMessage="Do you want to remove the selected stack(s)? Associated services will be removed as well."
className="!m-0"
data-cy="stack-removeStackButton" data-cy="stack-removeStackButton"
> />
Remove
</Button>
</Authorized> </Authorized>
<Authorized authorizations="PortainerStackCreate"> <Authorized authorizations="PortainerStackCreate">
<Button <AddButton data-cy="stack-addStackButton" to=".newstack">
as={Link}
props={{ to: '.newstack' }}
icon={Plus}
className="!m-0"
data-cy="stack-addStackButton"
>
Add stack Add stack
</Button> </AddButton>
</Authorized> </Authorized>
</div> </div>
); );

View File

@ -1,9 +1,7 @@
import { Plus, Trash2 } from 'lucide-react';
import { Authorized } from '@/react/hooks/useUser'; import { Authorized } from '@/react/hooks/useUser';
import { Link } from '@@/Link'; import { AddButton } from '@@/buttons';
import { Button } from '@@/buttons'; import { DeleteButton } from '@@/buttons/DeleteButton';
import { DecoratedVolume } from '../types'; import { DecoratedVolume } from '../types';
@ -17,27 +15,15 @@ export function TableActions({
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Authorized authorizations="DockerVolumeDelete"> <Authorized authorizations="DockerVolumeDelete">
<Button <DeleteButton
color="dangerlight"
disabled={selectedItems.length === 0} disabled={selectedItems.length === 0}
onClick={() => onRemove(selectedItems)} onConfirmed={() => onRemove(selectedItems)}
icon={Trash2} confirmMessage="Do you want to remove the selected volume(s)?"
className="!m-0"
data-cy="volume-removeVolumeButton" data-cy="volume-removeVolumeButton"
> />
Remove
</Button>
</Authorized> </Authorized>
<Authorized authorizations="DockerVolumeCreate"> <Authorized authorizations="DockerVolumeCreate">
<Button <AddButton data-cy="volume-addVolumeButton">Add volume</AddButton>
as={Link}
props={{ to: '.new' }}
icon={Plus}
className="!m-0"
data-cy="volume-addVolumeButton"
>
Add volume
</Button>
</Authorized> </Authorized>
</div> </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 { notifySuccess } from '@/portainer/services/notifications';
import { useDeleteEnvironmentsMutation } from '@/react/portainer/environments/queries/useDeleteEnvironmentsMutation'; 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 { useIsPureAdmin } from '@/react/hooks/useUser';
import { Button } from '@@/buttons'; import { Button } from '@@/buttons';
import { ModalType, openModal } from '@@/modals'; import { openModal } from '@@/modals';
import { confirm } from '@@/modals/confirm';
import { buildConfirmButton } from '@@/modals/utils';
import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren'; import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { useAssociateDeviceMutation, useLicenseOverused } from '../queries'; import { useAssociateDeviceMutation, useLicenseOverused } from '../queries';
import { WaitingRoomEnvironment } from '../types'; import { WaitingRoomEnvironment } from '../types';
@ -36,14 +35,13 @@ export function TableActions({
return ( return (
<> <>
<Button <DeleteButton
onClick={() => handleRemoveDevice(selectedRows)} onConfirmed={() => handleRemoveDevice(selectedRows)}
disabled={selectedRows.length === 0} disabled={selectedRows.length === 0}
color="dangerlight" confirmMessage="You're about to remove edge device(s) from waiting room, which will not be shown until next agent startup."
icon={Trash2}
> >
Remove Device Remove Device
</Button> </DeleteButton>
<TooltipWithChildren <TooltipWithChildren
message={ message={
@ -122,18 +120,6 @@ export function TableActions({
} }
async function handleRemoveDevice(devices: Environment[]) { 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( removeMutation.mutate(
devices.map((d) => d.Id), devices.map((d) => d.Id),
{ {

View File

@ -1,11 +1,7 @@
import { Trash2, Plus } from 'lucide-react';
import { notifySuccess } from '@/portainer/services/notifications'; import { notifySuccess } from '@/portainer/services/notifications';
import { Button } from '@@/buttons'; import { AddButton } from '@@/buttons';
import { confirmDestructive } from '@@/modals/confirm'; import { DeleteButton } from '@@/buttons/DeleteButton';
import { buildConfirmButton } from '@@/modals/utils';
import { Link } from '@@/Link';
import { useDeleteEdgeStacksMutation } from './useDeleteEdgeStacksMutation'; import { useDeleteEdgeStacksMutation } from './useDeleteEdgeStacksMutation';
import { DecoratedEdgeStack } from './types'; import { DecoratedEdgeStack } from './types';
@ -19,39 +15,17 @@ export function TableActions({
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <DeleteButton
color="dangerlight"
disabled={selectedItems.length === 0} disabled={selectedItems.length === 0}
onClick={() => handleRemove(selectedItems)} onConfirmed={() => handleRemove(selectedItems)}
icon={Trash2} confirmMessage="Are you sure you want to remove the selected Edge stack(s)?"
className="!m-0" />
>
Remove
</Button>
<Button <AddButton data-cy="edgeStack-addStackButton">Add stack</AddButton>
as={Link}
props={{ to: 'edge.stacks.new' }}
icon={Plus}
className="!m-0"
data-cy="edgeStack-addStackButton"
>
Add stack
</Button>
</div> </div>
); );
async function handleRemove(selectedItems: Array<DecoratedEdgeStack>) { 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); const ids = selectedItems.map((item) => item.Id);
removeMutation.mutate(ids, { removeMutation.mutate(ids, {
onSuccess: () => { onSuccess: () => {

View File

@ -1,4 +1,4 @@
import { Pencil, Plus } from 'lucide-react'; import { Pencil } from 'lucide-react';
import { useCurrentStateAndParams } from '@uirouter/react'; import { useCurrentStateAndParams } from '@uirouter/react';
import { Pod } from 'kubernetes-types/core/v1'; 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 { useNamespaceQuery } from '@/react/kubernetes/namespaces/queries/useNamespaceQuery';
import { Widget, WidgetBody } from '@@/Widget'; import { Widget, WidgetBody } from '@@/Widget';
import { Button } from '@@/buttons'; import { AddButton, Button } from '@@/buttons';
import { Link } from '@@/Link'; import { Link } from '@@/Link';
import { Icon } from '@@/Icon'; import { Icon } from '@@/Icon';
@ -102,23 +102,15 @@ export function ApplicationDetailsWidget() {
/> />
)} )}
{appStackFileQuery.data && ( {appStackFileQuery.data && (
<Link <AddButton
to="kubernetes.templates.custom.new" to="kubernetes.templates.custom.new"
data-cy="k8sAppDetail-createCustomTemplateButton"
params={{ params={{
fileContent: appStackFileQuery.data.StackFileContent, fileContent: appStackFileQuery.data.StackFileContent,
}} }}
> >
<Button Create template from application
type="button" </AddButton>
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>
)} )}
</div> </div>
)} )}

View File

@ -1,8 +1,6 @@
import { Trash2 } from 'lucide-react';
import { Authorized } from '@/react/hooks/useUser'; import { Authorized } from '@/react/hooks/useUser';
import { Button } from '@@/buttons'; import { DeleteButton } from '@@/buttons/DeleteButton';
import { KubernetesStack } from '../../types'; import { KubernetesStack } from '../../types';
@ -15,15 +13,12 @@ export function TableActions({
}) { }) {
return ( return (
<Authorized authorizations="K8sApplicationsW"> <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} disabled={selectedItems.length === 0}
color="dangerlight" onConfirmed={() => onRemove(selectedItems)}
onClick={() => onRemove(selectedItems)}
icon={Trash2}
data-cy="k8sApp-removeStackButton" data-cy="k8sApp-removeStackButton"
> />
Remove
</Button>
</Authorized> </Authorized>
); );
} }

View File

@ -1,5 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { FileCode, Plus, Trash2 } from 'lucide-react'; import { FileCode } from 'lucide-react';
import { ConfigMap } from 'kubernetes-types/core/v1'; import { ConfigMap } from 'kubernetes-types/core/v1';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
@ -12,12 +12,12 @@ import { Application } from '@/react/kubernetes/applications/types';
import { pluralize } from '@/portainer/helpers/strings'; import { pluralize } from '@/portainer/helpers/strings';
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery'; import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
import { Namespaces } from '@/react/kubernetes/namespaces/types'; import { Namespaces } from '@/react/kubernetes/namespaces/types';
import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton';
import { Datatable, TableSettingsMenu } from '@@/datatables'; import { Datatable, TableSettingsMenu } from '@@/datatables';
import { confirmDelete } from '@@/modals/confirm'; import { AddButton } from '@@/buttons';
import { Button } from '@@/buttons';
import { Link } from '@@/Link';
import { useTableState } from '@@/datatables/useTableState'; import { useTableState } from '@@/datatables/useTableState';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { import {
useConfigMapsForCluster, useConfigMapsForCluster,
@ -139,16 +139,6 @@ function TableActions({
const deleteConfigMapMutation = useMutationDeleteConfigMaps(environmentId); const deleteConfigMapMutation = useMutationDeleteConfigMaps(environmentId);
async function handleRemoveClick(configMaps: ConfigMap[]) { 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) => ({ const configMapsToDelete = configMaps.map((configMap) => ({
namespace: configMap.metadata?.namespace ?? '', namespace: configMap.metadata?.namespace ?? '',
name: configMap.metadata?.name ?? '', name: configMap.metadata?.name ?? '',
@ -159,41 +149,30 @@ function TableActions({
return ( return (
<Authorized authorizations="K8sConfigMapsW"> <Authorized authorizations="K8sConfigMapsW">
<Button <DeleteButton
className="btn-wrapper"
color="dangerlight"
disabled={selectedItems.length === 0} disabled={selectedItems.length === 0}
onClick={async () => { onConfirmed={() => handleRemoveClick(selectedItems)}
handleRemoveClick(selectedItems); confirmMessage={`Are you sure you want to remove the selected ${pluralize(
}} selectedItems.length,
icon={Trash2} 'ConfigMap'
)}`}
data-cy="k8sConfig-removeConfigButton" data-cy="k8sConfig-removeConfigButton"
/>
<AddButton
to="kubernetes.configmaps.new"
data-cy="k8sConfig-addConfigWithFormButton"
color="secondary"
> >
Remove Add with form
</Button> </AddButton>
<Link to="kubernetes.configmaps.new" className="ml-1">
<Button <CreateFromManifestButton
className="btn-wrapper"
color="secondary"
icon={Plus}
data-cy="k8sConfig-addConfigWithFormButton"
>
Add with form
</Button>
</Link>
<Link
to="kubernetes.deploy"
params={{ params={{
referrer: 'kubernetes.configurations',
tab: 'configmaps', tab: 'configmaps',
}} }}
className="ml-1"
data-cy="k8sConfig-deployFromManifestButton" data-cy="k8sConfig-deployFromManifestButton"
> />
<Button className="btn-wrapper" color="primary" icon={Plus}>
Create from manifest
</Button>
</Link>
</Authorized> </Authorized>
); );
} }

View File

@ -1,5 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Lock, Plus, Trash2 } from 'lucide-react'; import { Lock } from 'lucide-react';
import { Secret } from 'kubernetes-types/core/v1'; import { Secret } from 'kubernetes-types/core/v1';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
@ -12,12 +12,12 @@ import { Application } from '@/react/kubernetes/applications/types';
import { pluralize } from '@/portainer/helpers/strings'; import { pluralize } from '@/portainer/helpers/strings';
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery'; import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
import { Namespaces } from '@/react/kubernetes/namespaces/types'; import { Namespaces } from '@/react/kubernetes/namespaces/types';
import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton';
import { Datatable, TableSettingsMenu } from '@@/datatables'; import { Datatable, TableSettingsMenu } from '@@/datatables';
import { confirmDelete } from '@@/modals/confirm'; import { AddButton } from '@@/buttons';
import { Button } from '@@/buttons';
import { Link } from '@@/Link';
import { useTableState } from '@@/datatables/useTableState'; import { useTableState } from '@@/datatables/useTableState';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { import {
useSecretsForCluster, useSecretsForCluster,
@ -135,16 +135,6 @@ function TableActions({ selectedItems }: { selectedItems: SecretRowData[] }) {
const deleteSecretMutation = useMutationDeleteSecrets(environmentId); const deleteSecretMutation = useMutationDeleteSecrets(environmentId);
async function handleRemoveClick(secrets: SecretRowData[]) { 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) => ({ const secretsToDelete = secrets.map((secret) => ({
namespace: secret.metadata?.namespace ?? '', namespace: secret.metadata?.namespace ?? '',
name: secret.metadata?.name ?? '', name: secret.metadata?.name ?? '',
@ -155,41 +145,28 @@ function TableActions({ selectedItems }: { selectedItems: SecretRowData[] }) {
return ( return (
<Authorized authorizations="K8sSecretsW"> <Authorized authorizations="K8sSecretsW">
<Button <DeleteButton
className="btn-wrapper"
color="dangerlight"
disabled={selectedItems.length === 0} disabled={selectedItems.length === 0}
onClick={async () => { onConfirmed={() => handleRemoveClick(selectedItems)}
handleRemoveClick(selectedItems);
}}
icon={Trash2}
data-cy="k8sSecret-removeSecretButton" 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 Add with form
</Button> </AddButton>
<Link to="kubernetes.secrets.new" className="ml-1"> <CreateFromManifestButton
<Button
className="btn-wrapper"
color="secondary"
icon={Plus}
data-cy="k8sSecret-addSecretWithFormButton"
>
Add with form
</Button>
</Link>
<Link
to="kubernetes.deploy"
params={{ params={{
referrer: 'kubernetes.configurations',
tab: 'secrets', tab: 'secrets',
}} }}
className="ml-1"
data-cy="k8sSecret-deployFromManifestButton" data-cy="k8sSecret-deployFromManifestButton"
> />
<Button className="btn-wrapper" color="primary" icon={Plus}>
Create from manifest
</Button>
</Link>
</Authorized> </Authorized>
); );
} }

View File

@ -1,4 +1,3 @@
import { Plus, Trash2 } from 'lucide-react';
import { useRouter } from '@uirouter/react'; import { useRouter } from '@uirouter/react';
import { useMemo } from '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 { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription'; import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
import { confirmDelete } from '@@/modals/confirm';
import { Datatable, TableSettingsMenu } from '@@/datatables'; import { Datatable, TableSettingsMenu } from '@@/datatables';
import { Button } from '@@/buttons'; import { AddButton } from '@@/buttons';
import { Link } from '@@/Link';
import { useTableState } from '@@/datatables/useTableState'; import { useTableState } from '@@/datatables/useTableState';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { DeleteIngressesRequest, Ingress } from '../types'; import { DeleteIngressesRequest, Ingress } from '../types';
import { useDeleteIngresses, useIngresses } from '../queries'; import { useDeleteIngresses, useIngresses } from '../queries';
import { useNamespacesQuery } from '../../namespaces/queries/useNamespacesQuery'; import { useNamespacesQuery } from '../../namespaces/queries/useNamespacesQuery';
import { Namespaces } from '../../namespaces/types'; import { Namespaces } from '../../namespaces/types';
import { CreateFromManifestButton } from '../../components/CreateFromManifestButton';
import { columns } from './columns'; import { columns } from './columns';
@ -111,38 +110,20 @@ export function IngressDatatable() {
function tableActions(selectedFlatRows: Ingress[]) { function tableActions(selectedFlatRows: Ingress[]) {
return ( return (
<div className="ingressDatatable-actions"> <Authorized authorizations="K8sIngressesW">
<Authorized authorizations="AzureContainerGroupDelete"> <DeleteButton
<Button disabled={selectedFlatRows.length === 0}
color="dangerlight" onConfirmed={() => handleRemoveClick(selectedFlatRows)}
disabled={selectedFlatRows.length === 0} data-cy="k8sSecret-removeSecretButton"
onClick={() => handleRemoveClick(selectedFlatRows)} confirmMessage="Are you sure you want to delete the selected ingresses?"
icon={Trash2} />
>
Remove
</Button>
</Authorized>
<Authorized authorizations="K8sIngressesW"> <AddButton to=".create" color="secondary">
<Link Add with form
to="kubernetes.ingresses.create" </AddButton>
className="space-left no-decoration"
> <CreateFromManifestButton />
<Button icon={Plus} color="secondary"> </Authorized>
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>
); );
} }
@ -152,13 +133,6 @@ export function IngressDatatable() {
} }
async function handleRemoveClick(ingresses: SelectedIngress[]) { 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; const payload: DeleteIngressesRequest = {} as DeleteIngressesRequest;
ingresses.forEach((ingress) => { ingresses.forEach((ingress) => {
payload[ingress.Namespace] = payload[ingress.Namespace] || []; 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); background-color: var(--bg-body-color);
} }
.ingressDatatable-actions button > span,
.anntation-actions button > span, .anntation-actions button > span,
.rules-action button > span, .rules-action button > span,
.rule button > span { .rule button > span {

View File

@ -1,8 +1,8 @@
import { useMemo } from 'react'; import { Shuffle } from 'lucide-react';
import { Shuffle, Trash2 } from 'lucide-react';
import { useRouter } from '@uirouter/react'; import { useRouter } from '@uirouter/react';
import clsx from 'clsx'; import clsx from 'clsx';
import { Row } from '@tanstack/react-table'; import { Row } from '@tanstack/react-table';
import { useMemo } from 'react';
import { Namespaces } from '@/react/kubernetes/namespaces/types'; import { Namespaces } from '@/react/kubernetes/namespaces/types';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
@ -12,12 +12,11 @@ import { pluralize } from '@/portainer/helpers/strings';
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings'; import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription'; import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery'; import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton';
import { Datatable, Table, TableSettingsMenu } from '@@/datatables'; 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 { useTableState } from '@@/datatables/useTableState';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { import {
useMutationDeleteServices, 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( function useServicesRowData(
services: Service[], services: Service[],
namespaces?: Namespaces namespaces?: Namespaces
@ -136,26 +135,33 @@ function TableActions({ selectedItems }: TableActionsProps) {
const deleteServicesMutation = useMutationDeleteServices(environmentId); const deleteServicesMutation = useMutationDeleteServices(environmentId);
const router = useRouter(); const router = useRouter();
async function handleRemoveClick(services: SelectedService[]) { return (
const confirmed = await confirmDelete( <Authorized authorizations="K8sServicesW">
<> <DeleteButton
<p>{`Are you sure you want to remove the selected ${pluralize( disabled={selectedItems.length === 0}
services.length, onConfirmed={() => handleRemoveClick(selectedItems)}
'service' confirmMessage={
)}?`}</p> <>
<ul className="pl-6"> <p>{`Are you sure you want to remove the selected ${pluralize(
{services.map((s, index) => ( selectedItems.length,
<li key={index}> 'service'
{s.Namespace}/{s.Name} )}?`}</p>
</li> <ul className="pl-6">
))} {selectedItems.map((s, index) => (
</ul> <li key={index}>
</> {s.Namespace}/{s.Name}
); </li>
if (!confirmed) { ))}
return null; </ul>
} </>
}
/>
<CreateFromManifestButton />
</Authorized>
);
async function handleRemoveClick(services: SelectedService[]) {
const payload: Record<string, string[]> = {}; const payload: Record<string, string[]> = {};
services.forEach((service) => { services.forEach((service) => {
payload[service.Namespace] = payload[service.Namespace] || []; 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 { useRouter } from '@uirouter/react';
import { Trash2 } from 'lucide-react';
import { pluralize } from '@/portainer/helpers/strings'; import { pluralize } from '@/portainer/helpers/strings';
import { confirmDestructive } from '@@/modals/confirm'; import { AddButton } from '@@/buttons';
import { AddButton, Button } from '@@/buttons'; import { DeleteButton } from '@@/buttons/DeleteButton';
import { HelmRepository } from './types'; import { HelmRepository } from './types';
import { useDeleteHelmRepositoriesMutation } from './helm-repositories.service'; import { useDeleteHelmRepositoriesMutation } from './helm-repositories.service';
@ -18,37 +17,27 @@ export function HelmRepositoryDatatableActions({ selectedItems }: Props) {
const deleteHelmRepoMutation = useDeleteHelmRepositoriesMutation(); const deleteHelmRepoMutation = useDeleteHelmRepositoriesMutation();
return ( return (
<div className="flex gap-2"> <>
<Button <DeleteButton
disabled={selectedItems.length < 1} disabled={selectedItems.length === 0}
color="dangerlight" onConfirmed={() => onDeleteClick(selectedItems)}
onClick={() => onDeleteClick(selectedItems)} confirmMessage={`Are you sure you want to remove the selected Helm ${pluralize(
selectedItems.length,
'repository',
'repositories'
)}?`}
data-cy="credentials-deleteButton" 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 Add Helm repository
</AddButton> </AddButton>
</div> </>
); );
async function onDeleteClick(selectedItems: HelmRepository[]) { 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, { deleteHelmRepoMutation.mutate(selectedItems, {
onSuccess: () => { onSuccess: () => {
router.stateService.reload(); 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 { useState } from 'react';
import { useEnvironmentList } from '@/react/portainer/environments/queries'; 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 { Datatable } from '@@/datatables';
import { createPersistedStore } from '@@/datatables/types'; import { createPersistedStore } from '@@/datatables/types';
import { Button } from '@@/buttons'; import { AddButton, Button } from '@@/buttons';
import { Link } from '@@/Link';
import { useTableState } from '@@/datatables/useTableState'; import { useTableState } from '@@/datatables/useTableState';
import { isBE } from '../../feature-flags/feature-flags.service'; import { isBE } from '../../feature-flags/feature-flags.service';
@ -86,26 +85,20 @@ export function EnvironmentsDatatable({
<ImportFdoDeviceButton /> <ImportFdoDeviceButton />
{isBE && ( {isBE && (
<Button <AddButton
as={Link}
color="secondary" color="secondary"
icon={Plus} to="portainer.endpoints.edgeAutoCreateScript"
props={{ to: 'portainer.endpoints.edgeAutoCreateScript' }}
> >
Auto onboarding Auto onboarding
</Button> </AddButton>
)} )}
<Link to="portainer.wizard.endpoints">
<Button <AddButton
onClick={() => to="portainer.wizard.endpoints"
localStorage.setItem('wizardReferrer', 'environments') params={{ referrer: 'environments' }}
} >
icon={Plus} Add environment
className="!m-0" </AddButton>
>
Add environment
</Button>
</Link>
</div> </div>
)} )}
/> />

View File

@ -1,7 +1,4 @@
import { Plus } from 'lucide-react'; import { AddButton } from '@@/buttons';
import { Button } from '@@/buttons';
import { Link } from '@@/Link';
import { useSettings } from '../../settings/queries'; import { useSettings } from '../../settings/queries';
import { import {
@ -22,15 +19,10 @@ export function ImportFdoDeviceButton() {
} }
return ( return (
<Button <div className="ml-[5px]">
type="button" <AddButton color="secondary" to="portainer.endpoints.importDevice">
color="secondary" Import FDO device
icon={Plus} </AddButton>
as={Link} </div>
props={{ to: 'portainer.endpoints.importDevice' }}
className="ml-[5px]"
>
Import FDO device
</Button>
); );
} }

View File

@ -1,4 +1,4 @@
import { Clock, Trash2 } from 'lucide-react'; import { Clock } from 'lucide-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import _ from 'lodash'; import _ from 'lodash';
@ -6,12 +6,11 @@ import { notifySuccess } from '@/portainer/services/notifications';
import { withLimitToBE } from '@/react/hooks/useLimitToBE'; import { withLimitToBE } from '@/react/hooks/useLimitToBE';
import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups'; import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups';
import { confirmDelete } from '@@/modals/confirm';
import { Datatable } from '@@/datatables'; import { Datatable } from '@@/datatables';
import { PageHeader } from '@@/PageHeader'; import { PageHeader } from '@@/PageHeader';
import { Button } from '@@/buttons'; import { AddButton } from '@@/buttons';
import { Link } from '@@/Link';
import { useTableState } from '@@/datatables/useTableState'; import { useTableState } from '@@/datatables/useTableState';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { useList } from '../queries/list'; import { useList } from '../queries/list';
import { EdgeUpdateSchedule, StatusType } from '../types'; import { EdgeUpdateSchedule, StatusType } from '../types';
@ -90,29 +89,16 @@ function TableActions({
const removeMutation = useRemoveMutation(); const removeMutation = useRemoveMutation();
return ( return (
<> <>
<Button <DeleteButton
icon={Trash2} onConfirmed={() => handleRemove()}
color="dangerlight"
onClick={() => handleRemove()}
disabled={selectedRows.length === 0} disabled={selectedRows.length === 0}
> confirmMessage="Are you sure you want to remove these schedules?"
Remove />
</Button> <AddButton to=".create">Add update & rollback schedule</AddButton>
<Link to=".create">
<Button>Add update & rollback schedule</Button>
</Link>
</> </>
); );
async function handleRemove() { async function handleRemove() {
const confirmed = await confirmDelete(
'Are you sure you want to remove these?'
);
if (!confirmed) {
return;
}
removeMutation.mutate(selectedRows, { removeMutation.mutate(selectedRows, {
onSuccess: () => { onSuccess: () => {
notifySuccess('Success', 'Schedules successfully removed'); notifySuccess('Success', 'Schedules successfully removed');

View File

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

View File

@ -1,4 +1,4 @@
import { Bell, Trash2 } from 'lucide-react'; import { Bell } from 'lucide-react';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
import { useCurrentStateAndParams } from '@uirouter/react'; import { useCurrentStateAndParams } from '@uirouter/react';
@ -10,9 +10,9 @@ import { withReactQuery } from '@/react-tools/withReactQuery';
import { PageHeader } from '@@/PageHeader'; import { PageHeader } from '@@/PageHeader';
import { Datatable } from '@@/datatables'; import { Datatable } from '@@/datatables';
import { Button } from '@@/buttons';
import { createPersistedStore } from '@@/datatables/types'; import { createPersistedStore } from '@@/datatables/types';
import { useTableState } from '@@/datatables/useTableState'; import { useTableState } from '@@/datatables/useTableState';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { notificationsStore } from './notifications-store'; import { notificationsStore } from './notifications-store';
import { ToastNotification } from './types'; import { ToastNotification } from './types';
@ -62,14 +62,11 @@ function TableActions({ selectedRows }: { selectedRows: ToastNotification[] }) {
const { user } = useUser(); const { user } = useUser();
const notificationsStoreState = useStore(notificationsStore); const notificationsStoreState = useStore(notificationsStore);
return ( return (
<Button <DeleteButton
icon={Trash2} onConfirmed={() => handleRemove()}
color="dangerlight"
onClick={() => handleRemove()}
disabled={selectedRows.length === 0} disabled={selectedRows.length === 0}
> confirmMessage="Are you sure you want to remove the selected notifications?"
Remove />
</Button>
); );
function handleRemove() { function handleRemove() {

View File

@ -1,6 +1,6 @@
import { useQueryClient } from 'react-query'; import { useQueryClient } from 'react-query';
import { useRouter } from '@uirouter/react'; import { useRouter } from '@uirouter/react';
import { PlusCircle, Trash2 } from 'lucide-react'; import { PlusCircle } from 'lucide-react';
import { Profile } from '@/portainer/hostmanagement/fdo/model'; import { Profile } from '@/portainer/hostmanagement/fdo/model';
import * as notifications from '@/portainer/services/notifications'; import * as notifications from '@/portainer/services/notifications';
@ -9,10 +9,10 @@ import {
duplicateProfile, duplicateProfile,
} from '@/portainer/hostmanagement/fdo/fdo.service'; } from '@/portainer/hostmanagement/fdo/fdo.service';
import { confirm, confirmDestructive } from '@@/modals/confirm'; import { confirm } from '@@/modals/confirm';
import { Link } from '@@/Link'; import { Link } from '@@/Link';
import { Button } from '@@/buttons'; import { Button } from '@@/buttons';
import { buildConfirmButton } from '@@/modals/utils'; import { DeleteButton } from '@@/buttons/DeleteButton';
interface Props { interface Props {
isFDOEnabled: boolean; isFDOEnabled: boolean;
@ -27,7 +27,7 @@ export function FDOProfilesDatatableActions({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return ( return (
<div className="actionBar"> <>
<Link to="portainer.endpoints.profile" className="space-left"> <Link to="portainer.endpoints.profile" className="space-left">
<Button disabled={!isFDOEnabled} icon={PlusCircle}> <Button disabled={!isFDOEnabled} icon={PlusCircle}>
Add Profile Add Profile
@ -42,15 +42,12 @@ export function FDOProfilesDatatableActions({
Duplicate Duplicate
</Button> </Button>
<Button <DeleteButton
disabled={!isFDOEnabled || selectedItems.length < 1} disabled={!isFDOEnabled || selectedItems.length === 0}
color="danger" onConfirmed={() => onDeleteProfileClick()}
onClick={() => onDeleteProfileClick()} confirmMessage="This action will delete the selected profile(s). Continue?"
icon={Trash2} />
> </>
Remove
</Button>
</div>
); );
async function onDuplicateProfileClick() { async function onDuplicateProfileClick() {
@ -80,16 +77,6 @@ export function FDOProfilesDatatableActions({
} }
async function onDeleteProfileClick() { 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( await Promise.all(
selectedItems.map(async (profile) => { selectedItems.map(async (profile) => {
try { try {

View File

@ -2,7 +2,9 @@ import userEvent from '@testing-library/user-event';
import { PropsWithChildren } from 'react'; import { PropsWithChildren } from 'react';
import { render } from '@testing-library/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 { TemplateViewModel } from './view-model';
import { TemplateType } from './types'; import { TemplateType } from './types';
@ -15,13 +17,7 @@ test('should render AppTemplatesListItem component', () => {
const onSelect = vi.fn(); const onSelect = vi.fn();
const isSelected = false; const isSelected = false;
const { getByText } = render( const { getByText } = renderComponent({ isSelected, template, onSelect });
<AppTemplatesListItem
template={template}
onSelect={onSelect}
isSelected={isSelected}
/>
);
expect(getByText(template.Title, { exact: false })).toBeInTheDocument(); expect(getByText(template.Title, { exact: false })).toBeInTheDocument();
}); });
@ -45,26 +41,23 @@ const copyAsCustomTestCases = [
vi.mock('@uirouter/react', async (importOriginal: () => Promise<object>) => ({ vi.mock('@uirouter/react', async (importOriginal: () => Promise<object>) => ({
...(await importOriginal()), ...(await importOriginal()),
UISref: ({ children }: PropsWithChildren<unknown>) => children, // Mocking UISref to render its children directly 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 }) => { copyAsCustomTestCases.forEach(({ type, expected }) => {
test(`copy as custom button should ${ test(`copy as custom button should ${
expected ? '' : 'not ' expected ? '' : 'not '
}be rendered for type ${type}`, () => { }be rendered for type ${TemplateType[type]}`, () => {
const onSelect = vi.fn(); const onSelect = vi.fn();
const isSelected = false; const isSelected = false;
const { queryByText, unmount } = render( const { queryByText, unmount } = renderComponent({
<AppTemplatesListItem isSelected,
template={ template: {
{ Type: type,
Type: type, } as TemplateViewModel,
} as TemplateViewModel onSelect,
} });
onSelect={onSelect}
isSelected={isSelected}
/>
);
if (expected) { if (expected) {
expect(queryByText('Copy as Custom')).toBeVisible(); expect(queryByText('Copy as Custom')).toBeVisible();
@ -86,16 +79,34 @@ test('should call onSelect when clicked', async () => {
const onSelect = vi.fn(); const onSelect = vi.fn();
const isSelected = false; const isSelected = false;
const { getByLabelText } = render( const { getByLabelText } = renderComponent({
<AppTemplatesListItem isSelected,
template={template} template,
onSelect={onSelect} onSelect,
isSelected={isSelected} });
/>
);
const button = getByLabelText(template.Title); const button = getByLabelText(template.Title);
await user.click(button); await user.click(button);
expect(onSelect).toHaveBeenCalledWith(template); 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 _ from 'lodash';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
@ -9,8 +9,7 @@ import { Table } from '@@/datatables';
import { useTableState } from '@@/datatables/useTableState'; import { useTableState } from '@@/datatables/useTableState';
import { createPersistedStore } from '@@/datatables/types'; import { createPersistedStore } from '@@/datatables/types';
import { DatatableFooter } from '@@/datatables/DatatableFooter'; import { DatatableFooter } from '@@/datatables/DatatableFooter';
import { Button } from '@@/buttons'; import { AddButton } from '@@/buttons';
import { Link } from '@@/Link';
import { CustomTemplatesListItem } from './CustomTemplatesListItem'; import { CustomTemplatesListItem } from './CustomTemplatesListItem';
@ -56,11 +55,7 @@ export function CustomTemplatesList({
searchValue={listState.search} searchValue={listState.search}
title="Custom Templates" title="Custom Templates"
titleIcon={Edit} titleIcon={Edit}
renderTableActions={() => ( renderTableActions={() => <AddButton>Add Custom Template</AddButton>}
<Button as={Link} props={{ to: '.new' }} icon={Plus}>
Add Custom Template
</Button>
)}
/> />
<div className="blocklist gap-y-2 !px-[20px] !pb-[20px]" role="list"> <div className="blocklist gap-y-2 !px-[20px] !pb-[20px]" role="list">

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { useMutation, useQueryClient } from 'react-query'; import { useMutation, useQueryClient } from 'react-query';
import { Trash2, Users } from 'lucide-react'; import { Users } from 'lucide-react';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import { notifySuccess } from '@/portainer/services/notifications'; 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 { Team, TeamId } from '@/react/portainer/users/teams/types';
import { deleteTeam } from '@/react/portainer/users/teams/teams.service'; import { deleteTeam } from '@/react/portainer/users/teams/teams.service';
import { confirmDelete } from '@@/modals/confirm';
import { Datatable } from '@@/datatables'; import { Datatable } from '@@/datatables';
import { Button } from '@@/buttons';
import { buildNameColumn } from '@@/datatables/buildNameColumn'; import { buildNameColumn } from '@@/datatables/buildNameColumn';
import { createPersistedStore } from '@@/datatables/types'; import { createPersistedStore } from '@@/datatables/types';
import { useTableState } from '@@/datatables/useTableState'; import { useTableState } from '@@/datatables/useTableState';
import { DeleteButton } from '@@/buttons/DeleteButton';
const storageKey = 'teams'; const storageKey = 'teams';
@ -40,14 +39,11 @@ export function TeamsDatatable({ teams, isAdmin }: Props) {
titleIcon={Users} titleIcon={Users}
renderTableActions={(selectedRows) => renderTableActions={(selectedRows) =>
isAdmin && ( isAdmin && (
<Button <DeleteButton
color="dangerlight" onConfirmed={() => handleRemoveClick(selectedRows)}
onClick={() => handleRemoveClick(selectedRows)}
disabled={selectedRows.length === 0} disabled={selectedRows.length === 0}
icon={Trash2} confirmMessage="Are you sure you want to remove the selected teams?"
> />
Remove
</Button>
) )
} }
emptyContentLabel="No teams found" emptyContentLabel="No teams found"
@ -79,14 +75,6 @@ function useRemoveMutation() {
return { handleRemove }; return { handleRemove };
async function handleRemove(teams: TeamId[]) { 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, { deleteMutation.mutate(teams, {
onSuccess: () => { onSuccess: () => {
notifySuccess('Teams successfully removed', ''); notifySuccess('Teams successfully removed', '');