1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-08 23:35:31 +02:00

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

Co-authored-by: Chaim Lev-Ari <chaim.levi-ari@portaienr.io>
This commit is contained in:
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,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', '');