mirror of
https://github.com/portainer/portainer.git
synced 2025-08-09 15:55:23 +02:00
refactor(ui/modals): replace bootbox with react solution [EE-4541] (#8010)
This commit is contained in:
parent
392c7f74b8
commit
e66dea44e3
111 changed files with 1330 additions and 1562 deletions
|
@ -2,8 +2,15 @@
|
|||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.checkbox-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background-color: var(--white-color);
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:root[theme='dark'] .checkbox-list,
|
||||
:root[theme='highcontrast'] .checkbox-list {
|
||||
background-color: var(--bg-modal-content-color);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { X } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import { DialogContent, DialogOverlay } from '@reach/dialog';
|
||||
|
||||
import { downloadKubeconfigFile } from '@/react/kubernetes/services/kubeconfig.service';
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
|
@ -17,12 +15,12 @@ import {
|
|||
} from '@/react/portainer/environments/queries/useEnvironmentList';
|
||||
import { useListSelection } from '@/react/hooks/useListSelection';
|
||||
|
||||
import { Modal } from '@@/modals';
|
||||
import { PaginationControls } from '@@/PaginationControls';
|
||||
import { Checkbox } from '@@/form-components/Checkbox';
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import styles from './KubeconfigPrompt.module.css';
|
||||
import '@reach/dialog/styles.css';
|
||||
|
||||
export interface KubeconfigPromptProps {
|
||||
envQueryParams: Query;
|
||||
|
@ -63,90 +61,69 @@ export function KubeconfigPrompt({
|
|||
.every((env) => selection.includes(env.Id));
|
||||
|
||||
return (
|
||||
<DialogOverlay
|
||||
className={styles.dialog}
|
||||
aria-label="Kubeconfig View"
|
||||
role="dialog"
|
||||
onDismiss={onClose}
|
||||
>
|
||||
<DialogContent className="modal-dialog bg-transparent p-0">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" onClick={onClose}>
|
||||
<X />
|
||||
</button>
|
||||
<h5 className="modal-title">Download kubeconfig file</h5>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<form className="bootbox-form">
|
||||
<div className="bootbox-prompt-message">
|
||||
<span>
|
||||
Select the kubernetes environments to add to the kubeconfig
|
||||
file. You may select across multiple pages.
|
||||
</span>
|
||||
<span className="space-left">{expiryQuery.data}</span>
|
||||
</div>
|
||||
</form>
|
||||
<br />
|
||||
<div className="flex h-8 items-center">
|
||||
<Checkbox
|
||||
id="settings-container-truncate-name"
|
||||
label="Select all (in this page)"
|
||||
checked={isAllPageSelected}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
</div>
|
||||
<div className="datatable">
|
||||
<div className="bootbox-checkbox-list">
|
||||
{environments
|
||||
.filter((env) => env.Status <= 2)
|
||||
.map((env) => (
|
||||
<div
|
||||
key={env.Id}
|
||||
className={clsx(
|
||||
styles.checkbox,
|
||||
'flex h-8 items-center pt-1'
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
id={`${env.Id}`}
|
||||
label={`${env.Name} (${env.URL})`}
|
||||
checked={selection.includes(env.Id)}
|
||||
onChange={() =>
|
||||
toggleSelection(env.Id, !selection.includes(env.Id))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex w-full justify-end pt-3">
|
||||
<PaginationControls
|
||||
showAll={totalCount <= 100}
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
pageLimit={pageLimit}
|
||||
onPageLimitChange={setPageLimit}
|
||||
totalCount={totalCount}
|
||||
<Modal aria-label="Kubeconfig View" onDismiss={onClose}>
|
||||
<Modal.Header title="Download kubeconfig file" />
|
||||
|
||||
<Modal.Body>
|
||||
<div>
|
||||
<span>
|
||||
Select the kubernetes environments to add to the kubeconfig file.
|
||||
You may select across multiple pages.
|
||||
</span>
|
||||
<span className="space-left">{expiryQuery.data}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex h-8 items-center">
|
||||
<Checkbox
|
||||
id="settings-container-truncate-name"
|
||||
label="Select all (in this page)"
|
||||
checked={isAllPageSelected}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
</div>
|
||||
<div className="datatable">
|
||||
<div className={styles.checkboxList}>
|
||||
{environments
|
||||
.filter((env) => env.Status <= 2)
|
||||
.map((env) => (
|
||||
<div
|
||||
key={env.Id}
|
||||
className={clsx(
|
||||
styles.checkbox,
|
||||
'flex h-8 items-center pt-1'
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
id={`${env.Id}`}
|
||||
label={`${env.Name} (${env.URL})`}
|
||||
checked={!!selection[env.Id]}
|
||||
onChange={() => toggleSelection(env.Id, !selection[env.Id])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<Button onClick={onClose} color="default">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDownload}
|
||||
disabled={selection.length === 0}
|
||||
>
|
||||
Download File
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex w-full justify-end pt-3">
|
||||
<PaginationControls
|
||||
showAll={totalCount <= 100}
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
pageLimit={pageLimit}
|
||||
onPageLimitChange={setPageLimit}
|
||||
totalCount={totalCount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button onClick={onClose} color="default">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleDownload} disabled={selection.length === 0}>
|
||||
Download File
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
function handleSelectAll() {
|
||||
|
|
|
@ -5,10 +5,10 @@ import { Environment } from '@/react/portainer/environments/types';
|
|||
import { snapshotEndpoints } from '@/react/portainer/environments/environment.service';
|
||||
import { isEdgeEnvironment } from '@/react/portainer/environments/utils';
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import { confirmAsync } from '@/portainer/services/modal.service/confirm';
|
||||
import { buildTitle } from '@/portainer/services/modal.service/utils';
|
||||
|
||||
import { confirm } from '@@/modals/confirm';
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
import { ModalType } from '@@/modals';
|
||||
|
||||
import { EnvironmentList } from './EnvironmentList';
|
||||
import { EdgeLoadingSpinner } from './EdgeLoadingSpinner';
|
||||
|
@ -72,15 +72,10 @@ export function HomeView() {
|
|||
}
|
||||
|
||||
async function confirmEndpointSnapshot() {
|
||||
return confirmAsync({
|
||||
title: buildTitle('Are you sure?'),
|
||||
return confirm({
|
||||
title: 'Are you sure?',
|
||||
modalType: ModalType.Warn,
|
||||
message:
|
||||
'Triggering a manual refresh will poll each environment to retrieve its information, this may take a few moments.',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Continue',
|
||||
className: 'btn-primary',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,12 +4,14 @@ import { useMutation } from 'react-query';
|
|||
import { object } from 'yup';
|
||||
|
||||
import { useUser } from '@/react/hooks/useUser';
|
||||
import { confirmAsync } from '@/portainer/services/modal.service/confirm';
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
import { confirm } from '@@/modals/confirm';
|
||||
import { Button } from '@@/buttons';
|
||||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
import { ModalType } from '@@/modals';
|
||||
|
||||
import { EditDetails } from '../EditDetails';
|
||||
import { parseAccessControlFormData } from '../utils';
|
||||
|
@ -128,15 +130,11 @@ export function AccessControlPanelForm({
|
|||
}
|
||||
|
||||
function confirmAccessControlUpdate() {
|
||||
return confirmAsync({
|
||||
return confirm({
|
||||
modalType: ModalType.Warn,
|
||||
title: 'Are you sure?',
|
||||
message:
|
||||
'Changing the ownership of this resource will potentially restrict its management to some users.',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Change ownership',
|
||||
className: 'btn-primary',
|
||||
},
|
||||
},
|
||||
confirmButton: buildConfirmButton('Change ownership'),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { confirm } from '@@/modals/confirm';
|
||||
import { ModalType } from '@@/modals';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
|
||||
export function confirmDisassociate() {
|
||||
const message = (
|
||||
<>
|
||||
<p>
|
||||
Disassociating this Edge environment will mark it as non associated and
|
||||
will clear the registered Edge ID.
|
||||
</p>
|
||||
<p>
|
||||
Any agent started with the Edge key associated to this environment will
|
||||
be able to re-associate with this environment.
|
||||
</p>
|
||||
<p>
|
||||
You can re-use the Edge ID and Edge key that you used to deploy the
|
||||
existing Edge agent to associate a new Edge device to this environment.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
return confirm({
|
||||
title: 'About disassociating',
|
||||
modalType: ModalType.Warn,
|
||||
message,
|
||||
confirmButton: buildConfirmButton('Disassociate'),
|
||||
});
|
||||
}
|
|
@ -2,9 +2,9 @@ import { Clock, Trash2 } from 'lucide-react';
|
|||
import { useStore } from 'zustand';
|
||||
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm';
|
||||
import { withLimitToBE } from '@/react/hooks/useLimitToBE';
|
||||
|
||||
import { confirmDelete } from '@@/modals/confirm';
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
import { Button } from '@@/buttons';
|
||||
|
@ -91,7 +91,7 @@ function TableActions({
|
|||
);
|
||||
|
||||
async function handleRemove() {
|
||||
const confirmed = await confirmDeletionAsync(
|
||||
const confirmed = await confirmDelete(
|
||||
'Are you sure you want to remove these?'
|
||||
);
|
||||
if (!confirmed) {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { confirmDestructive } from '@/portainer/services/modal.service/confirm';
|
||||
import { Settings } from '@/react/portainer/settings/types';
|
||||
|
||||
import { confirmDestructive } from '@@/modals/confirm';
|
||||
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
|
||||
import { PasswordLengthSlider } from './PasswordLengthSlider/PasswordLengthSlider';
|
||||
import { SaveAuthSettingsButton } from './SaveAuthSettingsButton';
|
||||
|
@ -19,22 +20,18 @@ export function InternalAuth({
|
|||
value,
|
||||
onChange,
|
||||
}: Props) {
|
||||
function onSubmit() {
|
||||
async function onSubmit() {
|
||||
if (value.RequiredPasswordLength < 10) {
|
||||
confirmDestructive({
|
||||
const confirmed = await confirmDestructive({
|
||||
title: 'Allow weak passwords?',
|
||||
message:
|
||||
'You have set an insecure minimum password length. This could leave your system vulnerable to attack, are you sure?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Yes',
|
||||
className: 'btn-danger',
|
||||
},
|
||||
},
|
||||
callback: function onConfirm(confirmed) {
|
||||
if (confirmed) onSaveSettings();
|
||||
},
|
||||
confirmButton: buildConfirmButton('Yes', 'danger'),
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
onSaveSettings();
|
||||
}
|
||||
} else {
|
||||
onSaveSettings();
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { useField } from 'formik';
|
||||
|
||||
import { confirmAsync } from '@/portainer/services/modal.service/confirm';
|
||||
|
||||
import { confirm } from '@@/modals/confirm';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Switch } from '@@/form-components/SwitchField/Switch';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
import { ModalType } from '@@/modals';
|
||||
|
||||
export function EnabledWaitingRoomSwitch() {
|
||||
const [inputProps, meta, helpers] = useField<boolean>('TrustOnFirstConnect');
|
||||
|
@ -30,20 +31,12 @@ export function EnabledWaitingRoomSwitch() {
|
|||
return;
|
||||
}
|
||||
|
||||
const confirmed = await confirmAsync({
|
||||
const confirmed = await confirm({
|
||||
modalType: ModalType.Warn,
|
||||
title: 'Disable Edge Environment Waiting Room',
|
||||
message:
|
||||
'By disabling the waiting room feature, all devices requesting association will be automatically associated and could pose a security risk. Are you sure?',
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
className: 'btn-default',
|
||||
},
|
||||
confirm: {
|
||||
label: 'Confirm',
|
||||
className: 'btn-danger',
|
||||
},
|
||||
},
|
||||
confirmButton: buildConfirmButton('Confirm', 'danger'),
|
||||
});
|
||||
|
||||
helpers.setValue(!!confirmed);
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { useRouter } from '@uirouter/react';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
import { promptAsync } from '@/portainer/services/modal.service/prompt';
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
import { openModal } from '@@/modals';
|
||||
|
||||
import { usePublicSettings } from '../../queries';
|
||||
import { DeployTypePrompt } from './DeployTypePrompt';
|
||||
|
||||
enum DeployType {
|
||||
FDO = 'FDO',
|
||||
|
@ -42,30 +43,15 @@ export function AddDeviceButton() {
|
|||
}
|
||||
}
|
||||
|
||||
function getDeployType(): Promise<DeployType> {
|
||||
function getDeployType() {
|
||||
if (!isFDOEnabled) {
|
||||
return Promise.resolve(DeployType.MANUAL);
|
||||
}
|
||||
|
||||
return promptAsync({
|
||||
title: 'How would you like to add an Edge Device?',
|
||||
inputType: 'radio',
|
||||
inputOptions: [
|
||||
{
|
||||
text: 'Provision bare-metal using Intel FDO',
|
||||
value: DeployType.FDO,
|
||||
},
|
||||
{
|
||||
text: 'Deploy agent manually',
|
||||
value: DeployType.MANUAL,
|
||||
},
|
||||
],
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Confirm',
|
||||
className: 'btn-primary',
|
||||
},
|
||||
},
|
||||
}) as Promise<DeployType>;
|
||||
return askForDeployType();
|
||||
}
|
||||
}
|
||||
|
||||
function askForDeployType() {
|
||||
return openModal(DeployTypePrompt, {});
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { DeployType } from '@/react/nomad/jobs/JobsView/JobsDatatable/types';
|
||||
|
||||
import { OnSubmit } from '@@/modals';
|
||||
import { Dialog } from '@@/modals/Dialog';
|
||||
import { buildCancelButton, buildConfirmButton } from '@@/modals/utils';
|
||||
|
||||
export function DeployTypePrompt({
|
||||
onSubmit,
|
||||
}: {
|
||||
onSubmit: OnSubmit<DeployType>;
|
||||
}) {
|
||||
const [deployType, setDeployType] = useState<DeployType>(DeployType.FDO);
|
||||
return (
|
||||
<Dialog
|
||||
title="How would you like to add an Edge Device?"
|
||||
message={
|
||||
<>
|
||||
<RadioInput
|
||||
name="deployType"
|
||||
value={DeployType.FDO}
|
||||
label="Provision bare-metal using Intel FDO"
|
||||
groupValue={deployType}
|
||||
onChange={setDeployType}
|
||||
/>
|
||||
|
||||
<RadioInput
|
||||
name="deployType"
|
||||
value={DeployType.MANUAL}
|
||||
onChange={setDeployType}
|
||||
groupValue={deployType}
|
||||
label="Deploy agent manually"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
buttons={[buildCancelButton(), buildConfirmButton()]}
|
||||
onSubmit={(confirm) => onSubmit(confirm ? deployType : undefined)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RadioInput<T extends number | string>({
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
groupValue,
|
||||
name,
|
||||
}: {
|
||||
value: T;
|
||||
onChange: (value: T) => void;
|
||||
label: string;
|
||||
groupValue: T;
|
||||
name: string;
|
||||
}) {
|
||||
return (
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
className="!m-0"
|
||||
type="radio"
|
||||
name={name}
|
||||
value={value}
|
||||
checked={groupValue === value}
|
||||
onChange={() => onChange(value)}
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { AddDeviceButton } from './AddDeviceButton';
|
|
@ -3,18 +3,16 @@ import { useRouter } from '@uirouter/react';
|
|||
import { PlusCircle, Trash2 } from 'lucide-react';
|
||||
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
import {
|
||||
confirmAsync,
|
||||
confirmDestructiveAsync,
|
||||
} from '@/portainer/services/modal.service/confirm';
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import {
|
||||
deleteProfile,
|
||||
duplicateProfile,
|
||||
} from '@/portainer/hostmanagement/fdo/fdo.service';
|
||||
|
||||
import { confirm, confirmDestructive } from '@@/modals/confirm';
|
||||
import { Link } from '@@/Link';
|
||||
import { Button } from '@@/buttons';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
|
||||
interface Props {
|
||||
isFDOEnabled: boolean;
|
||||
|
@ -56,15 +54,9 @@ export function FDOProfilesDatatableActions({
|
|||
);
|
||||
|
||||
async function onDuplicateProfileClick() {
|
||||
const confirmed = await confirmAsync({
|
||||
const confirmed = await confirm({
|
||||
title: 'Are you sure ?',
|
||||
message: 'This action will duplicate the selected profile. Continue?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Confirm',
|
||||
className: 'btn-primary',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
|
@ -88,15 +80,10 @@ export function FDOProfilesDatatableActions({
|
|||
}
|
||||
|
||||
async function onDeleteProfileClick() {
|
||||
const confirmed = await confirmDestructiveAsync({
|
||||
title: 'Are you sure ?',
|
||||
const confirmed = await confirmDestructive({
|
||||
title: 'Are you sure?',
|
||||
message: 'This action will delete the selected profile(s). Continue?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Remove',
|
||||
className: 'btn-danger',
|
||||
},
|
||||
},
|
||||
confirmButton: buildConfirmButton('Remove', 'danger'),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
|
|
|
@ -2,7 +2,6 @@ import { useRouter } from '@uirouter/react';
|
|||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { Trash2, Users } from 'lucide-react';
|
||||
|
||||
import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm';
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||
import {
|
||||
mutationOptions,
|
||||
|
@ -10,6 +9,7 @@ import {
|
|||
withInvalidate,
|
||||
} from '@/react-tools/react-query';
|
||||
|
||||
import { confirmDelete } from '@@/modals/confirm';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Widget } from '@@/Widget';
|
||||
|
||||
|
@ -75,7 +75,7 @@ export function Details({ team, memberships, isAdmin }: Props) {
|
|||
);
|
||||
|
||||
async function handleDeleteClick() {
|
||||
const confirmed = await confirmDeletionAsync(
|
||||
const confirmed = await confirmDelete(
|
||||
`Do you want to delete this team? Users in this team will not be deleted.`
|
||||
);
|
||||
if (!confirmed) {
|
||||
|
|
|
@ -7,8 +7,8 @@ import { notifySuccess } from '@/portainer/services/notifications';
|
|||
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 { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm';
|
||||
|
||||
import { confirmDelete } from '@@/modals/confirm';
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { Button } from '@@/buttons';
|
||||
import { buildNameColumn } from '@@/datatables/NameCell';
|
||||
|
@ -86,7 +86,7 @@ function useRemoveMutation() {
|
|||
return { handleRemove };
|
||||
|
||||
async function handleRemove(teams: TeamId[]) {
|
||||
const confirmed = await confirmDeletionAsync(
|
||||
const confirmed = await confirmDelete(
|
||||
'Are you sure you want to remove the selected teams?'
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue