diff --git a/app/assets/css/bootstrap-override.css b/app/assets/css/bootstrap-override.css index 25121d7bf..973ea1248 100644 --- a/app/assets/css/bootstrap-override.css +++ b/app/assets/css/bootstrap-override.css @@ -247,19 +247,31 @@ input:checked + .slider:before { } .modal-content { - padding: 55px 20px 20px 20px; + padding: 20px; +} + +.background-error { + padding-top: 55px; + background-image: url(../images/icon-error.svg); + background-repeat: no-repeat; + background-position: top left; +} + +.background-warning { + padding-top: 55px; background-image: url(../images/icon-warning.svg); background-repeat: no-repeat; - background-position: top 10px left 10px; + background-position: top left; } .modal-header { - padding: 10px 0px 10px 0px; + margin-bottom: 10px; + padding: 0px; border-bottom: none; } .modal-header .close { - margin-top: -40px; + margin-top: 0px; } .modal-header .modal-title { diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 40f26b8db..7d651a9df 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -983,7 +983,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ function showConfirmationModal() { var deferred = $q.defer(); - ModalService.confirm({ + ModalService.confirmDestructive({ title: 'Are you sure ?', message: 'A container with the same name already exists. Portainer can automatically remove it and re-create one. Do you want to replace it?', buttons: { diff --git a/app/docker/views/services/edit/serviceController.js b/app/docker/views/services/edit/serviceController.js index 8f2f00c1c..48f5c70ee 100644 --- a/app/docker/views/services/edit/serviceController.js +++ b/app/docker/views/services/edit/serviceController.js @@ -550,7 +550,7 @@ angular.module('portainer.docker').controller('ServiceController', [ } $scope.rollbackService = function (service) { - ModalService.confirm({ + ModalService.confirmWarn({ title: 'Rollback service', message: 'Are you sure you want to rollback?', buttons: { diff --git a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableActions.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableActions.tsx index bb20186b1..a05db1151 100644 --- a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableActions.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableActions.tsx @@ -1,7 +1,10 @@ import { useRouter } from '@uirouter/react'; import type { Environment } from '@/portainer/environments/types'; -import { confirmAsync } from '@/portainer/services/modal.service/confirm'; +import { + confirmAsync, + confirmDestructiveAsync, +} from '@/portainer/services/modal.service/confirm'; import { promptAsync } from '@/portainer/services/modal.service/prompt'; import * as notifications from '@/portainer/services/notifications'; import { activateDevice } from '@/portainer/hostmanagement/open-amt/open-amt.service'; @@ -63,7 +66,7 @@ export function EdgeDevicesDatatableActions({ ); async function onDeleteEdgeDeviceClick() { - const confirmed = await confirmAsync({ + const confirmed = await confirmDestructiveAsync({ title: 'Are you sure ?', message: 'This action will remove all configurations associated to your environment(s). Continue?', diff --git a/app/edge/components/group-form/groupFormController.js b/app/edge/components/group-form/groupFormController.js index 55547ec61..7ff76bf18 100644 --- a/app/edge/components/group-form/groupFormController.js +++ b/app/edge/components/group-form/groupFormController.js @@ -1,5 +1,5 @@ import _ from 'lodash-es'; -import { confirmAsync } from '@/portainer/services/modal.service/confirm'; +import { confirmDestructiveAsync } from '@/portainer/services/modal.service/confirm'; import { EdgeTypes } from '@/portainer/environments/types'; import { getEnvironments } from '@/portainer/environments/environment.service'; @@ -50,7 +50,7 @@ export class EdgeGroupFormController { dissociateEndpoint(endpoint) { return this.$async(async () => { - const confirmed = await confirmAsync({ + const confirmed = await confirmDestructiveAsync({ title: 'Confirm action', message: 'Removing the environment from this group will remove its corresponding edge stacks', buttons: { diff --git a/app/portainer/home/HomeView.tsx b/app/portainer/home/HomeView.tsx index fa7e775bd..bd01e8858 100644 --- a/app/portainer/home/HomeView.tsx +++ b/app/portainer/home/HomeView.tsx @@ -10,6 +10,7 @@ import { Environment } from '../environments/types'; import { snapshotEndpoints } from '../environments/environment.service'; import { isEdgeEnvironment } from '../environments/utils'; import { confirmAsync } from '../services/modal.service/confirm'; +import { buildTitle } from '../services/modal.service/utils'; import { EnvironmentList } from './EnvironmentList'; import { EdgeLoadingSpinner } from './EdgeLoadingSpinner'; @@ -76,7 +77,7 @@ export const HomeViewAngular = r2a(HomeView, []); async function confirmEndpointSnapshot() { return confirmAsync({ - title: 'Are you sure?', + title: buildTitle('Are you sure?'), message: 'Triggering a manual refresh will poll each environment to retrieve its information, this may take a few moments.', buttons: { diff --git a/app/portainer/services/modal.service/confirm.ts b/app/portainer/services/modal.service/confirm.ts index a1aba183d..e8b25c234 100644 --- a/app/portainer/services/modal.service/confirm.ts +++ b/app/portainer/services/modal.service/confirm.ts @@ -1,7 +1,13 @@ import sanitize from 'sanitize-html'; import bootbox from 'bootbox'; -import { applyBoxCSS, ButtonsOptions, confirmButtons } from './utils'; +import { + applyBoxCSS, + ButtonsOptions, + confirmButtons, + buildTitle, + ModalTypeIcon, +} from './utils'; type ConfirmCallback = (confirmed: boolean) => void; @@ -17,7 +23,7 @@ interface ConfirmOptions extends ConfirmAsyncOptions { export function confirmWebEditorDiscard() { const options = { - title: 'Are you sure ?', + title: buildTitle('Are you sure?'), message: 'You currently have unsaved changes in the editor. Are you sure you want to leave?', buttons: { @@ -39,6 +45,17 @@ export function confirmAsync(options: ConfirmAsyncOptions) { return new Promise((resolve) => { confirm({ ...options, + title: buildTitle(options.title), + callback: (confirmed) => resolve(confirmed), + }); + }); +} + +export function confirmDestructiveAsync(options: ConfirmAsyncOptions) { + return new Promise((resolve) => { + confirm({ + ...options, + title: buildTitle(options.title, ModalTypeIcon.Destructive), callback: (confirmed) => resolve(confirmed), }); }); @@ -55,9 +72,20 @@ export function confirm(options: ConfirmOptions) { applyBoxCSS(box); } +export function confirmWarn(options: ConfirmOptions) { + confirm({ ...options, title: buildTitle(options.title, ModalTypeIcon.Warn) }); +} + +export function confirmDestructive(options: ConfirmOptions) { + confirm({ + ...options, + title: buildTitle(options.title, ModalTypeIcon.Destructive), + }); +} + export function confirmImageForceRemoval(callback: ConfirmCallback) { confirm({ - title: 'Are you sure?', + title: buildTitle('Are you sure?', ModalTypeIcon.Destructive), message: 'Forcing the removal of the image will remove the image even if it has multiple tags or if it is used by stopped containers.', buttons: { @@ -72,7 +100,7 @@ export function confirmImageForceRemoval(callback: ConfirmCallback) { export function cancelRegistryRepositoryAction(callback: ConfirmCallback) { confirm({ - title: 'Are you sure?', + title: buildTitle('Are you sure?', ModalTypeIcon.Destructive), message: 'WARNING: interrupting this operation before it has finished will result in the loss of all tags. Are you sure you want to do this?', buttons: { @@ -88,7 +116,7 @@ export function cancelRegistryRepositoryAction(callback: ConfirmCallback) { export function confirmDeletion(message: string, callback: ConfirmCallback) { const messageSanitized = sanitize(message); confirm({ - title: 'Are you sure ?', + title: buildTitle('Are you sure?', ModalTypeIcon.Destructive), message: messageSanitized, buttons: { confirm: { @@ -107,7 +135,7 @@ export function confirmWithTitle( ) { const messageSanitized = sanitize(message); confirm({ - title: sanitize(title), + title: buildTitle(title, ModalTypeIcon.Destructive), message: messageSanitized, buttons: { confirm: { @@ -122,7 +150,7 @@ export function confirmWithTitle( export function confirmDetachment(message: string, callback: ConfirmCallback) { const messageSanitized = sanitize(message); confirm({ - title: 'Are you sure ?', + title: buildTitle('Are you sure?'), message: messageSanitized, buttons: { confirm: { @@ -140,7 +168,7 @@ export function confirmDisassociate(callback: ConfirmCallback) { '

Any agent started with the Edge key associated to this environment will be able to re-associate with this environment.

' + '

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.

'; confirm({ - title: 'About disassociating', + title: buildTitle('About disassociating'), message: sanitize(message), buttons: { confirm: { @@ -156,7 +184,7 @@ export function confirmUpdate(message: string, callback: ConfirmCallback) { const messageSanitized = sanitize(message); confirm({ - title: 'Are you sure ?', + title: buildTitle('Are you sure?'), message: messageSanitized, buttons: { confirm: { @@ -195,7 +223,7 @@ export function confirmDeletionAsync(message: string) { export function confirmImageExport(callback: ConfirmCallback) { confirm({ - title: 'Caution', + title: buildTitle('Caution'), message: 'The export may take several minutes, do not navigate away whilst the export is in progress.', buttons: { @@ -210,7 +238,7 @@ export function confirmImageExport(callback: ConfirmCallback) { export function confirmChangePassword() { return confirmAsync({ - title: 'Are you sure?', + title: buildTitle('Are you sure?'), message: 'You will be logged out after the password change. Do you want to change your password?', buttons: { diff --git a/app/portainer/services/modal.service/index.ts b/app/portainer/services/modal.service/index.ts index 1def4d400..1fc4ffad6 100644 --- a/app/portainer/services/modal.service/index.ts +++ b/app/portainer/services/modal.service/index.ts @@ -4,6 +4,9 @@ import bootbox from 'bootbox'; import { cancelRegistryRepositoryAction, confirmAsync, + confirmWarn, + confirmDestructive, + confirmDestructiveAsync, confirmDisassociate, confirmDeletion, confirmDetachment, @@ -42,6 +45,9 @@ export function ModalServiceAngular() { enlargeImage, confirmWebEditorDiscard, confirmAsync, + confirmWarn, + confirmDestructive, + confirmDestructiveAsync, confirm, confirmImageForceRemoval, cancelRegistryRepositoryAction, diff --git a/app/portainer/services/modal.service/prompt.ts b/app/portainer/services/modal.service/prompt.ts index bb3cae7ea..489dc711e 100644 --- a/app/portainer/services/modal.service/prompt.ts +++ b/app/portainer/services/modal.service/prompt.ts @@ -1,8 +1,13 @@ import sanitize from 'sanitize-html'; import bootbox from 'bootbox'; -import '@@/BoxSelector/BoxSelectorItem.css'; -import { applyBoxCSS, ButtonsOptions, confirmButtons } from './utils'; +import { + applyBoxCSS, + ButtonsOptions, + confirmButtons, + buildTitle, + ModalTypeIcon, +} from './utils'; type PromptCallback = ((value: string) => void) | ((value: string[]) => void); @@ -60,10 +65,8 @@ export function confirmContainerDeletion( title: string, callback: PromptCallback ) { - const sanitizedTitle = sanitize(title); - prompt({ - title: sanitizedTitle, + title: buildTitle(title, ModalTypeIcon.Destructive), inputType: 'checkbox', inputOptions: [ { @@ -90,7 +93,7 @@ export function confirmContainerRecreation( callback: PromptCallback ) { const box = prompt({ - title: 'Are you sure?', + title: buildTitle('Are you sure?', ModalTypeIcon.Destructive), inputType: 'checkbox', inputOptions: [ @@ -137,7 +140,7 @@ export function confirmServiceForceUpdate( const sanitizedMessage = sanitize(message); const box = prompt({ - title: 'Are you sure?', + title: buildTitle('Are you sure?'), inputType: 'checkbox', inputOptions: [ { @@ -164,8 +167,10 @@ export function confirmStackUpdate( confirmButtonClassName: string | undefined, callback: PromptCallback ) { + const sanitizedMessage = sanitize(message); + const box = prompt({ - title: 'Are you sure?', + title: buildTitle('Are you sure?'), inputType: 'checkbox', inputOptions: [ { @@ -181,7 +186,7 @@ export function confirmStackUpdate( }, callback, }); - box.find('.bootbox-body').prepend(message); + box.find('.bootbox-body').prepend(sanitizedMessage); const checkbox = box.find('.bootbox-input-checkbox'); checkbox.prop('checked', defaultToggle); checkbox.prop('disabled', defaultDisabled); diff --git a/app/portainer/services/modal.service/utils.ts b/app/portainer/services/modal.service/utils.ts index 7fb92bfd4..d7ba033b7 100644 --- a/app/portainer/services/modal.service/utils.ts +++ b/app/portainer/services/modal.service/utils.ts @@ -10,6 +10,11 @@ export interface ButtonsOptions { cancel?: Button; } +export enum ModalTypeIcon { + Warn = 'warning', + Destructive = 'error', +} + export function confirmButtons(options: ButtonsOptions) { return { confirm: { @@ -27,6 +32,17 @@ export function confirmButtons(options: ButtonsOptions) { }; } +export function buildTitle( + title: string, + modalType: ModalTypeIcon = ModalTypeIcon.Warn +) { + return ` +
+ +
+ `; +} + export function applyBoxCSS(box: JQuery) { box.css({ 'vertical-align': 'middle', diff --git a/app/portainer/settings/authentication/internal-auth/InternalAuth.tsx b/app/portainer/settings/authentication/internal-auth/InternalAuth.tsx index ef684fd60..c55ef9700 100644 --- a/app/portainer/settings/authentication/internal-auth/InternalAuth.tsx +++ b/app/portainer/settings/authentication/internal-auth/InternalAuth.tsx @@ -1,5 +1,5 @@ import { react2angular } from '@/react-tools/react2angular'; -import { confirm } from '@/portainer/services/modal.service/confirm'; +import { confirmDestructive } from '@/portainer/services/modal.service/confirm'; import { FormSectionTitle } from '@@/form-components/FormSectionTitle'; @@ -23,7 +23,7 @@ export function InternalAuth({ }: Props) { function onSubmit() { if (value.RequiredPasswordLength < 10) { - confirm({ + 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?', diff --git a/app/portainer/settings/edge-compute/FDOProfilesDatatable/FDOProfilesDatatableActions.tsx b/app/portainer/settings/edge-compute/FDOProfilesDatatable/FDOProfilesDatatableActions.tsx index 4be3671be..f757b8b1c 100644 --- a/app/portainer/settings/edge-compute/FDOProfilesDatatable/FDOProfilesDatatableActions.tsx +++ b/app/portainer/settings/edge-compute/FDOProfilesDatatable/FDOProfilesDatatableActions.tsx @@ -2,7 +2,10 @@ import { useQueryClient } from 'react-query'; import { useRouter } from '@uirouter/react'; import { Profile } from '@/portainer/hostmanagement/fdo/model'; -import { confirmAsync } from '@/portainer/services/modal.service/confirm'; +import { + confirmAsync, + confirmDestructiveAsync, +} from '@/portainer/services/modal.service/confirm'; import * as notifications from '@/portainer/services/notifications'; import { deleteProfile, @@ -86,7 +89,7 @@ export function FDOProfilesDatatableActions({ } async function onDeleteProfileClick() { - const confirmed = await confirmAsync({ + const confirmed = await confirmDestructiveAsync({ title: 'Are you sure ?', message: 'This action will delete the selected profile(s). Continue?', buttons: { diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index 9e168618a..4e0dcefe0 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -5,7 +5,7 @@ import { PortainerEndpointTypes } from '@/portainer/models/endpoint/models'; import { EndpointSecurityFormData } from '@/portainer/components/endpointSecurity/porEndpointSecurityModel'; import EndpointHelper from '@/portainer/helpers/endpointHelper'; import { getAMTInfo } from 'Portainer/hostmanagement/open-amt/open-amt.service'; -import { confirmAsync } from '@/portainer/services/modal.service/confirm'; +import { confirmDestructiveAsync } from '@/portainer/services/modal.service/confirm'; import { isEdgeEnvironment } from '@/portainer/environments/utils'; import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts'; @@ -192,7 +192,7 @@ function EndpointController( var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only'); if (isEdgeEnvironment(endpoint.Type) && _.difference($scope.initialTagIds, endpoint.TagIds).length > 0) { - let confirmed = await confirmAsync({ + let confirmed = await confirmDestructiveAsync({ title: 'Confirm action', message: 'Removing tags from this environment will remove the corresponding edge stacks when dynamic grouping is being used', buttons: { diff --git a/app/portainer/views/stacks/edit/stackController.js b/app/portainer/views/stacks/edit/stackController.js index dbceefff7..a8bd60368 100644 --- a/app/portainer/views/stacks/edit/stackController.js +++ b/app/portainer/views/stacks/edit/stackController.js @@ -120,7 +120,7 @@ angular.module('portainer.app').controller('StackController', [ $scope.migrateStack = function (name, endpointId) { return $q(function (resolve) { - ModalService.confirm({ + ModalService.confirmWarn({ title: 'Are you sure?', message: 'This action will deploy a new instance of this stack on the target environment, please note that this does NOT relocate the content of any persistent volumes that may be attached to this stack.', diff --git a/app/portainer/views/users/edit/userController.js b/app/portainer/views/users/edit/userController.js index 567a70325..fcac81852 100644 --- a/app/portainer/views/users/edit/userController.js +++ b/app/portainer/views/users/edit/userController.js @@ -36,7 +36,7 @@ angular.module('portainer.app').controller('UserController', [ let promise = Promise.resolve(true); if (username != oldUsername) { promise = new Promise((resolve) => - ModalService.confirm({ + ModalService.confirmWarn({ title: 'Are you sure?', message: `Are you sure you want to rename the user ${oldUsername} to ${username}?`, buttons: {