mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +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
|
@ -3,8 +3,8 @@ import { useStore } from 'zustand';
|
|||
|
||||
import { ContainerGroup } from '@/react/azure/types';
|
||||
import { Authorized } from '@/react/hooks/useUser';
|
||||
import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm';
|
||||
|
||||
import { confirmDelete } from '@@/modals/confirm';
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Link } from '@@/Link';
|
||||
|
@ -63,7 +63,7 @@ export function ContainersDatatable({ dataset, onRemoveClick }: Props) {
|
|||
);
|
||||
|
||||
async function handleRemoveClick(containerIds: string[]) {
|
||||
const confirmed = await confirmDeletionAsync(
|
||||
const confirmed = await confirmDelete(
|
||||
'Are you sure you want to delete the selected containers?'
|
||||
);
|
||||
if (!confirmed) {
|
||||
|
|
|
@ -22,6 +22,7 @@ type Color =
|
|||
| 'light'
|
||||
| 'dangerlight'
|
||||
| 'warninglight'
|
||||
| 'warning'
|
||||
| 'none';
|
||||
type Size = 'xsmall' | 'small' | 'medium' | 'large';
|
||||
|
||||
|
|
|
@ -67,9 +67,10 @@ export function SingleSelect<TValue = string>({
|
|||
isClearable,
|
||||
bindToBody,
|
||||
}: SingleProps<TValue>) {
|
||||
const selectedValue = value
|
||||
? _.first(findSelectedOptions<TValue>(options, value))
|
||||
: null;
|
||||
const selectedValue =
|
||||
value || (typeof value === 'number' && value === 0)
|
||||
? _.first(findSelectedOptions<TValue>(options, value))
|
||||
: null;
|
||||
|
||||
return (
|
||||
<ReactSelect<Option<TValue>>
|
||||
|
@ -94,7 +95,8 @@ function findSelectedOptions<TValue>(
|
|||
value: TValue | readonly TValue[]
|
||||
) {
|
||||
const valueArr = Array.isArray(value) ? value : [value];
|
||||
return _.compact(
|
||||
|
||||
const values = _.compact(
|
||||
options.flatMap((option) => {
|
||||
if (isGroup(option)) {
|
||||
return option.options.find((option) => valueArr.includes(option.value));
|
||||
|
@ -107,6 +109,8 @@ function findSelectedOptions<TValue>(
|
|||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
export function MultiSelect<TValue = string>({
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/* switch box */
|
||||
|
||||
.switch,
|
||||
.bootbox-checkbox-list > .checkbox > label {
|
||||
.switch {
|
||||
--switch-size: 24px;
|
||||
}
|
||||
|
||||
|
@ -13,8 +12,7 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.switch i,
|
||||
.bootbox-form .checkbox i {
|
||||
.switch i {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
|
@ -27,8 +25,7 @@
|
|||
box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.switch i:before,
|
||||
.bootbox-form .checkbox i:before {
|
||||
.switch i:before {
|
||||
display: block;
|
||||
content: '';
|
||||
width: var(--switch-size);
|
||||
|
@ -38,8 +35,7 @@
|
|||
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.switch :checked + i,
|
||||
.bootbox-form .checkbox :checked ~ i {
|
||||
.switch :checked + i {
|
||||
padding-right: 0;
|
||||
padding-left: var(--switch-size);
|
||||
-webkit-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
|
||||
|
|
56
app/react/components/modals/Dialog.tsx
Normal file
56
app/react/components/modals/Dialog.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { ReactNode } from 'react';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { ButtonOptions, ModalType } from './types';
|
||||
import { openModal } from './open-modal';
|
||||
import { Modal, OnSubmit } from './Modal';
|
||||
|
||||
export interface DialogOptions<T> {
|
||||
title?: ReactNode;
|
||||
message: ReactNode;
|
||||
modalType?: ModalType;
|
||||
buttons: Array<ButtonOptions<T>>;
|
||||
}
|
||||
|
||||
interface Props<T> extends DialogOptions<T> {
|
||||
onSubmit: OnSubmit<T>;
|
||||
}
|
||||
|
||||
export function Dialog<T>({
|
||||
buttons,
|
||||
message,
|
||||
title,
|
||||
onSubmit,
|
||||
modalType,
|
||||
}: Props<T>) {
|
||||
const ariaLabel = requireString(title) || requireString(message) || 'Dialog';
|
||||
|
||||
return (
|
||||
<Modal onDismiss={() => onSubmit()} aria-label={ariaLabel}>
|
||||
{title && <Modal.Header title={title} modalType={modalType} />}
|
||||
<Modal.Body>{message}</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{buttons.map((button, index) => (
|
||||
<Button
|
||||
onClick={() => onSubmit(button.value)}
|
||||
className={button.className}
|
||||
color={button.color}
|
||||
key={index}
|
||||
size="medium"
|
||||
>
|
||||
{button.label}
|
||||
</Button>
|
||||
))}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function requireString(value: ReactNode) {
|
||||
return typeof value === 'string' ? value : undefined;
|
||||
}
|
||||
|
||||
export async function openDialog<T>(options: DialogOptions<T>) {
|
||||
return openModal<DialogOptions<T>, T>(Dialog, options);
|
||||
}
|
74
app/react/components/modals/SwitchPrompt.tsx
Normal file
74
app/react/components/modals/SwitchPrompt.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { ReactNode, useState } from 'react';
|
||||
|
||||
import { SwitchField } from '@@/form-components/SwitchField';
|
||||
|
||||
import { ModalType, type ButtonOptions } from './types';
|
||||
import { openModal } from './open-modal';
|
||||
import { OnSubmit } from './Modal/types';
|
||||
import { Dialog } from './Dialog';
|
||||
import { buildCancelButton, buildConfirmButton } from './utils';
|
||||
|
||||
function SwitchPrompt({
|
||||
onSubmit,
|
||||
title,
|
||||
confirmButton = buildConfirmButton('OK'),
|
||||
switchLabel,
|
||||
modalType,
|
||||
message,
|
||||
defaultValue = false,
|
||||
}: {
|
||||
onSubmit: OnSubmit<{ value: boolean }>;
|
||||
title: string;
|
||||
switchLabel: string;
|
||||
confirmButton?: ButtonOptions<true>;
|
||||
modalType?: ModalType;
|
||||
message?: ReactNode;
|
||||
defaultValue?: boolean;
|
||||
}) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
modalType={modalType}
|
||||
title={title}
|
||||
message={
|
||||
<>
|
||||
{message && <div className="mb-3">{message}</div>}
|
||||
<SwitchField
|
||||
name="value"
|
||||
label={switchLabel}
|
||||
checked={value}
|
||||
onChange={setValue}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
onSubmit={(confirm) => onSubmit(confirm ? { value } : undefined)}
|
||||
buttons={[buildCancelButton(), confirmButton]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export async function openSwitchPrompt(
|
||||
title: string,
|
||||
switchLabel: string,
|
||||
{
|
||||
confirmButton,
|
||||
modalType,
|
||||
message,
|
||||
defaultValue,
|
||||
}: {
|
||||
confirmButton?: ButtonOptions<true>;
|
||||
modalType?: ModalType;
|
||||
message?: ReactNode;
|
||||
defaultValue?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
return openModal(SwitchPrompt, {
|
||||
confirmButton,
|
||||
title,
|
||||
switchLabel,
|
||||
modalType,
|
||||
message,
|
||||
defaultValue,
|
||||
});
|
||||
}
|
80
app/react/components/modals/confirm.ts
Normal file
80
app/react/components/modals/confirm.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { openDialog, DialogOptions } from './Dialog';
|
||||
import { OnSubmit, ModalType } from './Modal';
|
||||
import { ButtonOptions } from './types';
|
||||
import { buildCancelButton, buildConfirmButton } from './utils';
|
||||
|
||||
export type ConfirmCallback = OnSubmit<boolean>;
|
||||
|
||||
export interface ConfirmOptions
|
||||
extends Omit<DialogOptions<boolean>, 'title' | 'buttons'> {
|
||||
title: string;
|
||||
confirmButton?: ButtonOptions<true>;
|
||||
cancelButtonLabel?: string;
|
||||
}
|
||||
|
||||
export async function openConfirm({
|
||||
confirmButton = buildConfirmButton(),
|
||||
cancelButtonLabel,
|
||||
...options
|
||||
}: ConfirmOptions) {
|
||||
const result = await openDialog({
|
||||
...options,
|
||||
buttons: [buildCancelButton(cancelButtonLabel), confirmButton],
|
||||
});
|
||||
return !!result;
|
||||
}
|
||||
|
||||
export function confirm(options: ConfirmOptions) {
|
||||
return openConfirm(options);
|
||||
}
|
||||
|
||||
export function confirmDestructive(options: Omit<ConfirmOptions, 'modalType'>) {
|
||||
return openConfirm({
|
||||
...options,
|
||||
modalType: ModalType.Destructive,
|
||||
});
|
||||
}
|
||||
|
||||
export function confirmWebEditorDiscard() {
|
||||
return openConfirm({
|
||||
modalType: ModalType.Warn,
|
||||
title: 'Are you sure?',
|
||||
message:
|
||||
'You currently have unsaved changes in the editor. Are you sure you want to leave?',
|
||||
confirmButton: buildConfirmButton('Yes', 'danger'),
|
||||
});
|
||||
}
|
||||
|
||||
export function confirmDelete(message: string) {
|
||||
return confirmDestructive({
|
||||
title: 'Are you sure?',
|
||||
message,
|
||||
confirmButton: buildConfirmButton('Remove', 'danger'),
|
||||
});
|
||||
}
|
||||
|
||||
export async function confirmUpdate(
|
||||
message: string,
|
||||
callback: ConfirmCallback
|
||||
) {
|
||||
const result = await openConfirm({
|
||||
title: 'Are you sure?',
|
||||
modalType: ModalType.Warn,
|
||||
message,
|
||||
confirmButton: buildConfirmButton('Update'),
|
||||
});
|
||||
|
||||
callback(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function confirmChangePassword() {
|
||||
return openConfirm({
|
||||
modalType: ModalType.Warn,
|
||||
title: 'Are you sure?',
|
||||
message:
|
||||
'You will be logged out after the password change. Do you want to change your password?',
|
||||
confirmButton: buildConfirmButton('Change'),
|
||||
});
|
||||
}
|
4
app/react/components/modals/index.ts
Normal file
4
app/react/components/modals/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export { Modal } from './Modal';
|
||||
export { openModal } from './open-modal';
|
||||
export { ModalType } from './types';
|
||||
export { type OnSubmit } from './Modal/types';
|
29
app/react/components/modals/open-modal.tsx
Normal file
29
app/react/components/modals/open-modal.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { ComponentType } from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
|
||||
import '@reach/dialog/styles.css';
|
||||
import { OnSubmit } from './Modal/types';
|
||||
|
||||
let counter = 0;
|
||||
export async function openModal<TProps, TResult>(
|
||||
Modal: ComponentType<{ onSubmit: OnSubmit<TResult> } & TProps>,
|
||||
props: TProps = {} as TProps
|
||||
) {
|
||||
const modal = document.createElement('div');
|
||||
counter += 1;
|
||||
modal.id = `dialog-${counter}`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const result = await new Promise<TResult | undefined>((resolve) => {
|
||||
render(
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<Modal {...props} onSubmit={(result) => resolve(result)} />,
|
||||
modal
|
||||
);
|
||||
});
|
||||
|
||||
unmountComponentAtNode(modal);
|
||||
document.body.removeChild(modal);
|
||||
|
||||
return result;
|
||||
}
|
20
app/react/components/modals/types.ts
Normal file
20
app/react/components/modals/types.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { ComponentProps } from 'react';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
export interface ButtonOptions<TValue = undefined> {
|
||||
label: string;
|
||||
className?: string;
|
||||
color?: ComponentProps<typeof Button>['color'];
|
||||
value?: TValue;
|
||||
}
|
||||
|
||||
export interface ButtonsOptions<T> {
|
||||
confirm: ButtonOptions<T>;
|
||||
cancel?: ButtonOptions<T>;
|
||||
}
|
||||
|
||||
export enum ModalType {
|
||||
Warn = 'warning',
|
||||
Destructive = 'error',
|
||||
}
|
20
app/react/components/modals/utils.ts
Normal file
20
app/react/components/modals/utils.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { ComponentProps } from 'react';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { ButtonOptions } from './types';
|
||||
|
||||
export function buildConfirmButton(
|
||||
label = 'Confirm',
|
||||
color: ComponentProps<typeof Button>['color'] = 'primary'
|
||||
): ButtonOptions<true> {
|
||||
return { label, color, value: true };
|
||||
}
|
||||
|
||||
export function buildCancelButton(label = 'Cancel'): ButtonOptions<false> {
|
||||
return {
|
||||
label,
|
||||
color: 'default',
|
||||
value: false,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { Modal, OnSubmit, ModalType, openModal } from '@@/modals';
|
||||
import { Button } from '@@/buttons';
|
||||
import { SwitchField } from '@@/form-components/SwitchField';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
|
||||
interface Props {
|
||||
onSubmit: OnSubmit<{ pullLatest: boolean }>;
|
||||
|
||||
cannotPullImage: boolean;
|
||||
}
|
||||
|
||||
function ConfirmRecreationModal({ onSubmit, cannotPullImage }: Props) {
|
||||
const [pullLatest, setPullLatest] = useState(false);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onDismiss={() => onSubmit()}
|
||||
aria-label="confirm recreate container modal"
|
||||
>
|
||||
<Modal.Header title="Are you sure?" modalType={ModalType.Destructive} />
|
||||
|
||||
<Modal.Body>
|
||||
<p>
|
||||
You're about to recreate this container and any non-persisted
|
||||
data will be lost. This container will be removed and another one will
|
||||
be created using the same configuration.
|
||||
</p>
|
||||
<SwitchField
|
||||
name="pullLatest"
|
||||
label="Re-pull image"
|
||||
checked={pullLatest}
|
||||
onChange={setPullLatest}
|
||||
disabled={cannotPullImage}
|
||||
/>
|
||||
{cannotPullImage && (
|
||||
<div className="mt-1 text-sm">
|
||||
<TextTip color="orange">
|
||||
Cannot re-pull as the image is inaccessible - either it no longer
|
||||
exists or the tag or name is no longer correct.
|
||||
</TextTip>
|
||||
</div>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={() => onSubmit()} color="default">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => onSubmit({ pullLatest })} color="danger">
|
||||
Recreate
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export async function confirmContainerRecreation(cannotPullImage: boolean) {
|
||||
return openModal(ConfirmRecreationModal, {
|
||||
cannotPullImage,
|
||||
});
|
||||
}
|
|
@ -11,7 +11,7 @@ import {
|
|||
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import { useAuthorizations, Authorized } from '@/react/hooks/useUser';
|
||||
import { confirmContainerDeletion } from '@/portainer/services/modal.service/prompt';
|
||||
import { confirmContainerDeletion } from '@/react/docker/containers/common/confirm-container-delete-modal';
|
||||
import { setPortainerAgentTargetHeader } from '@/portainer/services/http-request.helper';
|
||||
import {
|
||||
ContainerId,
|
||||
|
@ -242,7 +242,7 @@ export function ContainersDatatableActions({
|
|||
);
|
||||
}
|
||||
|
||||
function onRemoveClick(selectedItems: DockerContainer[]) {
|
||||
async function onRemoveClick(selectedItems: DockerContainer[]) {
|
||||
const isOneContainerRunning = selectedItems.some(
|
||||
(container) => container.State === 'running'
|
||||
);
|
||||
|
@ -250,14 +250,13 @@ export function ContainersDatatableActions({
|
|||
const runningTitle = isOneContainerRunning ? 'running' : '';
|
||||
const title = `You are about to remove one or more ${runningTitle} containers.`;
|
||||
|
||||
confirmContainerDeletion(title, (result: string[]) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const cleanVolumes = !!result[0];
|
||||
const result = await confirmContainerDeletion(title);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const { removeVolumes } = result;
|
||||
|
||||
removeSelectedContainers(selectedItems, cleanVolumes);
|
||||
});
|
||||
removeSelectedContainers(selectedItems, removeVolumes);
|
||||
}
|
||||
|
||||
async function executeActionOnContainerList(
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { ModalType } from '@@/modals';
|
||||
import { openSwitchPrompt } from '@@/modals/SwitchPrompt';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
|
||||
export async function confirmContainerDeletion(title: string) {
|
||||
const result = await openSwitchPrompt(
|
||||
title,
|
||||
'Automatically remove non-persistent volumes',
|
||||
{
|
||||
confirmButton: buildConfirmButton('Remove', 'danger'),
|
||||
modalType: ModalType.Destructive,
|
||||
}
|
||||
);
|
||||
|
||||
return result ? { removeVolumes: result.value } : undefined;
|
||||
}
|
58
app/react/docker/images/ItemView/RegistrySelectPrompt.tsx
Normal file
58
app/react/docker/images/ItemView/RegistrySelectPrompt.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { Registry } from '@/react/portainer/environments/environment.service/registries';
|
||||
|
||||
import { Modal, OnSubmit, openModal } from '@@/modals';
|
||||
import { Button } from '@@/buttons';
|
||||
import { PortainerSelect } from '@@/form-components/PortainerSelect';
|
||||
|
||||
interface Props {
|
||||
registries: Registry[];
|
||||
onSubmit: OnSubmit<Registry['Id']>;
|
||||
defaultValue: Registry['Id'];
|
||||
}
|
||||
|
||||
function RegistrySelectPrompt({ onSubmit, defaultValue, registries }: Props) {
|
||||
const title = 'Which registry do you want to use?';
|
||||
const [registryId, setRegistryId] = useState(defaultValue);
|
||||
const options = registries2Options(registries);
|
||||
|
||||
return (
|
||||
<Modal onDismiss={() => onSubmit()} aria-label={title}>
|
||||
<Modal.Header title={title} />
|
||||
|
||||
<Modal.Body>
|
||||
<PortainerSelect
|
||||
onChange={setRegistryId}
|
||||
value={registryId}
|
||||
options={options}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={() => onSubmit()} color="default">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => onSubmit(registryId)} color="primary">
|
||||
Update
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function selectRegistry(
|
||||
registries: Registry[],
|
||||
defaultValue: Registry['Id']
|
||||
) {
|
||||
return openModal(RegistrySelectPrompt, {
|
||||
registries,
|
||||
defaultValue,
|
||||
});
|
||||
}
|
||||
|
||||
function registries2Options(registries: Registry[]) {
|
||||
return registries.map((r) => ({
|
||||
label: r.Name,
|
||||
value: r.Id,
|
||||
}));
|
||||
}
|
15
app/react/docker/images/common/ConfirmExportModal.tsx
Normal file
15
app/react/docker/images/common/ConfirmExportModal.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { ModalType } from '@@/modals';
|
||||
import { ConfirmCallback, openConfirm } from '@@/modals/confirm';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
|
||||
export async function confirmImageExport(callback: ConfirmCallback) {
|
||||
const result = await openConfirm({
|
||||
modalType: ModalType.Warn,
|
||||
title: 'Caution',
|
||||
message:
|
||||
'The export may take several minutes, do not navigate away whilst the export is in progress.',
|
||||
confirmButton: buildConfirmButton('Continue'),
|
||||
});
|
||||
|
||||
callback(result);
|
||||
}
|
|
@ -4,13 +4,13 @@ import { useQueryClient } from 'react-query';
|
|||
import _ from 'lodash';
|
||||
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm';
|
||||
import { AccessControlPanel } from '@/react/portainer/access-control/AccessControlPanel/AccessControlPanel';
|
||||
import { ResourceControlType } from '@/react/portainer/access-control/types';
|
||||
import { DockerContainer } from '@/react/docker/containers/types';
|
||||
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
||||
import { useContainers } from '@/react/docker/containers/queries/containers';
|
||||
|
||||
import { confirmDelete } from '@@/modals/confirm';
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
|
||||
import { useNetwork, useDeleteNetwork } from '../queries';
|
||||
|
@ -103,7 +103,7 @@ export function ItemView() {
|
|||
|
||||
async function onRemoveNetworkClicked() {
|
||||
const message = 'Do you want to delete the network?';
|
||||
const confirmed = await confirmDeletionAsync(message);
|
||||
const confirmed = await confirmDelete(message);
|
||||
|
||||
if (confirmed) {
|
||||
deleteNetworkMutation.mutate(
|
||||
|
|
13
app/react/docker/services/common/update-service-modal.ts
Normal file
13
app/react/docker/services/common/update-service-modal.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { openSwitchPrompt } from '@@/modals/SwitchPrompt';
|
||||
import { ModalType } from '@@/modals';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
|
||||
export async function confirmServiceForceUpdate(message: string) {
|
||||
const result = await openSwitchPrompt('Are you sure?', 'Re-pull image', {
|
||||
message,
|
||||
confirmButton: buildConfirmButton('Update'),
|
||||
modalType: ModalType.Warn,
|
||||
});
|
||||
|
||||
return result ? { pullLatest: result.value } : undefined;
|
||||
}
|
21
app/react/docker/stacks/common/confirm-stack-update.ts
Normal file
21
app/react/docker/stacks/common/confirm-stack-update.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { openSwitchPrompt } from '@@/modals/SwitchPrompt';
|
||||
import { ModalType } from '@@/modals';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
|
||||
export async function confirmStackUpdate(
|
||||
message: string,
|
||||
defaultValue: boolean
|
||||
) {
|
||||
const result = await openSwitchPrompt(
|
||||
'Are you sure?',
|
||||
'Re-pull image and redeploy',
|
||||
{
|
||||
message,
|
||||
confirmButton: buildConfirmButton('Update'),
|
||||
modalType: ModalType.Warn,
|
||||
defaultValue,
|
||||
}
|
||||
);
|
||||
|
||||
return result ? { pullImage: result.value } : undefined;
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { Modal, openModal } from '@@/modals';
|
||||
import { Button } from '@@/buttons';
|
||||
import { SwitchField } from '@@/form-components/SwitchField';
|
||||
|
||||
function UpdateIngressPrompt({
|
||||
onSubmit,
|
||||
title,
|
||||
hasOneIngress,
|
||||
hasOnePort,
|
||||
}: {
|
||||
onSubmit: (value?: { noMatch: boolean }) => void;
|
||||
title: string;
|
||||
hasOneIngress: boolean;
|
||||
hasOnePort: boolean;
|
||||
}) {
|
||||
const [value, setValue] = useState(false);
|
||||
|
||||
const rulePlural = !hasOneIngress ? 'rules' : 'rule';
|
||||
const noMatchSentence = !hasOnePort
|
||||
? `Service ports in this application no longer match the ingress ${rulePlural}.`
|
||||
: `A service port in this application no longer matches the ingress ${rulePlural} which may break ingress rule paths.`;
|
||||
const inputLabel = `Update ingress ${rulePlural} to match the service port changes`;
|
||||
|
||||
return (
|
||||
<Modal onDismiss={() => onSubmit()} aria-label={title}>
|
||||
<Modal.Header title={title} />
|
||||
|
||||
<Modal.Body>
|
||||
<ul className="ml-3">
|
||||
<li>Updating the application may cause a service interruption.</li>
|
||||
<li>{noMatchSentence}</li>
|
||||
</ul>
|
||||
|
||||
<SwitchField
|
||||
name="noMatch"
|
||||
label={inputLabel}
|
||||
checked={value}
|
||||
onChange={setValue}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={() => onSubmit({ noMatch: value })} color="primary">
|
||||
Update
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function confirmUpdateAppIngress(
|
||||
ingressesToUpdate: Array<unknown>,
|
||||
servicePortsToUpdate: Array<unknown>
|
||||
) {
|
||||
const hasOneIngress = ingressesToUpdate.length === 1;
|
||||
const hasOnePort = servicePortsToUpdate.length === 1;
|
||||
|
||||
return openModal(UpdateIngressPrompt, {
|
||||
title: 'Are you sure?',
|
||||
hasOneIngress,
|
||||
hasOnePort,
|
||||
});
|
||||
}
|
|
@ -2,13 +2,14 @@ import { useEffect, useState } from 'react';
|
|||
import { AlertTriangle, Database } from 'lucide-react';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { confirmWarn } from '@/portainer/services/modal.service/confirm';
|
||||
|
||||
import { confirm } from '@@/modals/confirm';
|
||||
import { ModalType } from '@@/modals';
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { Button, ButtonGroup } from '@@/buttons';
|
||||
import { Icon } from '@@/Icon';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
|
||||
import { IngressControllerClassMap } from '../types';
|
||||
|
||||
|
@ -158,7 +159,7 @@ export function IngressClassDatatable({
|
|||
);
|
||||
}
|
||||
|
||||
function updateIngressControllers(
|
||||
async function updateIngressControllers(
|
||||
selectedRows: IngressControllerClassMap[],
|
||||
ingControllerFormValues: IngressControllerClassMap[],
|
||||
availability: boolean
|
||||
|
@ -194,38 +195,32 @@ export function IngressClassDatatable({
|
|||
);
|
||||
|
||||
if (usedControllersToDisallow.length > 0) {
|
||||
const usedControllerHtmlListItems = usedControllersToDisallow.map(
|
||||
(controller) => `<li>${controller.ClassName}</li>`
|
||||
);
|
||||
const usedControllerHtmlList = `<ul class="ml-6">${usedControllerHtmlListItems.join(
|
||||
''
|
||||
)}</ul>`;
|
||||
confirmWarn({
|
||||
const confirmed = await confirm({
|
||||
title: 'Disallow in-use ingress controllers?',
|
||||
message: `
|
||||
modalType: ModalType.Warn,
|
||||
message: (
|
||||
<div>
|
||||
<p>There are ingress controllers you want to disallow that are in use:</p>
|
||||
${usedControllerHtmlList}
|
||||
<p>No new ingress rules can be created for the disallowed controllers.</p>
|
||||
</div>`,
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
className: 'btn-default',
|
||||
},
|
||||
confirm: {
|
||||
label: 'Disallow',
|
||||
className: 'btn-warning',
|
||||
},
|
||||
},
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
setIngControllerFormValues(updatedIngressControllers);
|
||||
onChangeControllers(updatedIngressControllers);
|
||||
}
|
||||
},
|
||||
<p>
|
||||
There are ingress controllers you want to disallow that are in
|
||||
use:
|
||||
</p>
|
||||
<ul className="ml-6">
|
||||
{usedControllersToDisallow.map((controller) => (
|
||||
<li key={controller.ClassName}>${controller.ClassName}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>
|
||||
No new ingress rules can be created for the disallowed
|
||||
controllers.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
confirmButton: buildConfirmButton('Disallow', 'warning'),
|
||||
});
|
||||
return;
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setIngControllerFormValues(updatedIngressControllers);
|
||||
onChangeControllers(updatedIngressControllers);
|
||||
|
|
|
@ -5,9 +5,9 @@ import { useStore } from 'zustand';
|
|||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
import { useNamespaces } from '@/react/kubernetes/namespaces/queries';
|
||||
import { useAuthorizations, Authorized } from '@/react/hooks/useUser';
|
||||
import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm';
|
||||
import Route from '@/assets/ico/route.svg?c';
|
||||
|
||||
import { confirmDelete } from '@@/modals/confirm';
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Link } from '@@/Link';
|
||||
|
@ -110,7 +110,7 @@ export function IngressDatatable() {
|
|||
}
|
||||
|
||||
async function handleRemoveClick(ingresses: SelectedIngress[]) {
|
||||
const confirmed = await confirmDeletionAsync(
|
||||
const confirmed = await confirmDelete(
|
||||
'Are you sure you want to delete the selected ingresses?'
|
||||
);
|
||||
if (!confirmed) {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { confirm } from '@@/modals/confirm';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
|
||||
export function confirmRedeploy() {
|
||||
return confirm({
|
||||
title: '',
|
||||
message: (
|
||||
<>
|
||||
One or multiple applications are currently using this volume.
|
||||
<br /> For the change to be taken into account these applications will
|
||||
need to be redeployed. Do you want us to reschedule it now?
|
||||
</>
|
||||
),
|
||||
confirmButton: buildConfirmButton('Redeploy the applications'),
|
||||
cancelButtonLabel: "I'll do it later",
|
||||
});
|
||||
}
|
|
@ -3,8 +3,8 @@ import { Trash2 } from 'lucide-react';
|
|||
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
import { Job } from '@/react/nomad/types';
|
||||
import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm';
|
||||
|
||||
import { confirmDelete } from '@@/modals/confirm';
|
||||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||
|
||||
import { deleteJobs } from './delete';
|
||||
|
@ -33,7 +33,7 @@ export function JobActions({ selectedItems, refreshData }: Props) {
|
|||
);
|
||||
|
||||
async function handleDeleteClicked() {
|
||||
const confirmed = await confirmDeletionAsync(
|
||||
const confirmed = await confirmDelete(
|
||||
'Are you sure to delete all selected jobs?'
|
||||
);
|
||||
|
||||
|
|
|
@ -6,3 +6,8 @@ import {
|
|||
export interface TableSettings
|
||||
extends BasicTableSettings,
|
||||
RefreshableTableSettings {}
|
||||
|
||||
export enum DeployType {
|
||||
FDO = 'FDO',
|
||||
MANUAL = 'MANUAL',
|
||||
}
|
||||
|
|
|
@ -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?'
|
||||
);
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { useState } from 'react';
|
||||
import { Database, Hash, Server, Tag, Wrench } from 'lucide-react';
|
||||
import { DialogOverlay } from '@reach/dialog';
|
||||
|
||||
import { useSystemStatus } from '@/react/portainer/system/useSystemStatus';
|
||||
import { useSystemVersion } from '@/react/portainer/system/useSystemVersion';
|
||||
|
||||
import { Modal } from '@@/modals';
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import styles from './Footer.module.css';
|
||||
|
@ -48,77 +48,66 @@ function BuildInfoModal({ closeModal }: { closeModal: () => void }) {
|
|||
const { ServerVersion, DatabaseVersion, Build } = versionQuery.data;
|
||||
|
||||
return (
|
||||
<DialogOverlay className={styles.dialog} isOpen>
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" onClick={closeModal}>
|
||||
×
|
||||
</button>
|
||||
<h5 className="modal-title">Portainer {Edition}</h5>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className={styles.versionInfo}>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<span className="inline-flex items-center">
|
||||
<Server size="13" className="space-right" />
|
||||
Server Version: {ServerVersion}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className="inline-flex items-center">
|
||||
<Database size="13" className="space-right" />
|
||||
Database Version: {DatabaseVersion}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span className="inline-flex items-center">
|
||||
<Hash size="13" className="space-right" />
|
||||
CI Build Number: {Build.BuildNumber}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
<Tag size="13" className="space-right" />
|
||||
Image Tag: {Build.ImageTag}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className={styles.toolsList}>
|
||||
<span className="inline-flex items-center">
|
||||
<Wrench size="13" className="space-right" />
|
||||
Compilation tools:
|
||||
</span>
|
||||
<Modal onDismiss={closeModal} aria-label="build-info-modal">
|
||||
<Modal.Header title={`Portainer ${Edition}`} />
|
||||
<Modal.Body>
|
||||
<div className={styles.versionInfo}>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<span className="inline-flex items-center">
|
||||
<Server size="13" className="space-right" />
|
||||
Server Version: {ServerVersion}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className="inline-flex items-center">
|
||||
<Database size="13" className="space-right" />
|
||||
Database Version: {DatabaseVersion}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span className="inline-flex items-center">
|
||||
<Hash size="13" className="space-right" />
|
||||
CI Build Number: {Build.BuildNumber}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
<Tag size="13" className="space-right" />
|
||||
Image Tag: {Build.ImageTag}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className={styles.toolsList}>
|
||||
<span className="inline-flex items-center">
|
||||
<Wrench size="13" className="space-right" />
|
||||
Compilation tools:
|
||||
</span>
|
||||
|
||||
<div className={styles.tools}>
|
||||
<span className="text-muted small">
|
||||
Nodejs v{Build.NodejsVersion}
|
||||
</span>
|
||||
<span className="text-muted small">
|
||||
Yarn v{Build.YarnVersion}
|
||||
</span>
|
||||
<span className="text-muted small">
|
||||
Webpack v{Build.WebpackVersion}
|
||||
</span>
|
||||
<span className="text-muted small">Go v{Build.GoVersion}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<Button className="bootbox-accept" onClick={closeModal}>
|
||||
Ok
|
||||
</Button>
|
||||
<div className={styles.tools}>
|
||||
<span className="text-muted small">
|
||||
Nodejs v{Build.NodejsVersion}
|
||||
</span>
|
||||
<span className="text-muted small">Yarn v{Build.YarnVersion}</span>
|
||||
<span className="text-muted small">
|
||||
Webpack v{Build.WebpackVersion}
|
||||
</span>
|
||||
<span className="text-muted small">Go v{Build.GoVersion}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogOverlay>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button className="w-full" onClick={closeModal}>
|
||||
Ok
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue