diff --git a/.prettierrc b/.prettierrc
index 86d3c52f2..184ed56cb 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -13,10 +13,11 @@
},
{
"files": [
- "*.{j,t}sx"
+ "*.{j,t}sx",
+ "*.ts"
],
"options": {
- "printWidth": 80,
+ "printWidth": 80
}
}
]
diff --git a/.storybook/preview.js b/.storybook/preview.js
index 9a31caa06..127a08b28 100644
--- a/.storybook/preview.js
+++ b/.storybook/preview.js
@@ -1,5 +1,7 @@
import '../app/assets/css';
+import { pushStateLocationPlugin, UIRouter } from '@uirouter/react';
+
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
@@ -9,3 +11,11 @@ export const parameters = {
},
},
};
+
+export const decorators = [
+ (Story) => (
+
De-associating this Edge environment will mark it as non associated and will clear the registered Edge ID.
' + + '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 de-associating', + message: sanitize(message), + buttons: { + confirm: { + label: 'De-associate', + className: 'btn-primary', + }, + }, + callback, + }); +} + +export function confirmUpdate(message: string, callback: ConfirmCallback) { + const messageSanitized = sanitize(message); + + confirm({ + title: 'Are you sure ?', + message: messageSanitized, + buttons: { + confirm: { + label: 'Update', + className: 'btn-warning', + }, + }, + callback, + }); +} + +export function confirmRedeploy(message: string, callback: ConfirmCallback) { + const messageSanitized = sanitize(message); + + confirm({ + title: '', + message: messageSanitized, + buttons: { + confirm: { + label: 'Redeploy the applications', + className: 'btn-primary', + }, + cancel: { + label: "I'll do it later", + }, + }, + callback, + }); +} + +export function confirmDeletionAsync(message: string) { + return new Promise((resolve) => { + confirmDeletion(message, (confirmed) => resolve(confirmed)); + }); +} + +export function confirmEndpointSnapshot(callback: ConfirmCallback) { + confirm({ + title: 'Are you sure?', + 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', + }, + }, + callback, + }); +} + +export function confirmImageExport(callback: ConfirmCallback) { + confirm({ + title: 'Caution', + message: + 'The export may take several minutes, do not navigate away whilst the export is in progress.', + buttons: { + confirm: { + label: 'Continue', + className: 'btn-primary', + }, + }, + callback, + }); +} diff --git a/app/portainer/services/modal.service/index.ts b/app/portainer/services/modal.service/index.ts new file mode 100644 index 000000000..b27596d65 --- /dev/null +++ b/app/portainer/services/modal.service/index.ts @@ -0,0 +1,60 @@ +import sanitize from 'sanitize-html'; +import bootbox from 'bootbox'; + +import { + cancelRegistryRepositoryAction, + confirmAccessControlUpdate, + confirmAsync, + confirmDeassociate, + confirmDeletion, + confirmDeletionAsync, + confirmEndpointSnapshot, + confirmImageExport, + confirmImageForceRemoval, + confirmRedeploy, + confirmUpdate, + confirmWebEditorDiscard, + confirm, +} from './confirm'; +import { + confirmContainerDeletion, + confirmContainerRecreation, + confirmServiceForceUpdate, + confirmKubeconfigSelection, + selectRegistry, +} from './prompt'; + +export function enlargeImage(imageUrl: string) { + const imageSanitized = sanitize(imageUrl); + + bootbox.dialog({ + message: `${message}
`); + const checkbox = box.find('.bootbox-input-checkbox'); + checkbox.prop('checked', toggleCheckbox); + + if (showCheck) { + checkbox.addClass('visible'); + } +} diff --git a/app/portainer/services/modal.service/utils.ts b/app/portainer/services/modal.service/utils.ts new file mode 100644 index 000000000..043818a13 --- /dev/null +++ b/app/portainer/services/modal.service/utils.ts @@ -0,0 +1,37 @@ +import sanitize from 'sanitize-html'; + +interface Button { + label: string; + className?: string; +} + +export interface ButtonsOptions { + confirm: Button; + cancel?: Button; +} + +export function confirmButtons(options: ButtonsOptions) { + return { + confirm: { + label: sanitize(options.confirm.label), + className: + options.confirm.className && sanitize(options.confirm.className), + }, + cancel: { + label: + options.cancel && options.cancel.label + ? sanitize(options.cancel.label) + : 'Cancel', + }, + }; +} + +export function applyBoxCSS(box: JQuery' + options.message + '
'); - box.find('.bootbox-input-checkbox').prop('checked', options.optionToggled); - if (options.showCheck) { - box.find('.bootbox-input-checkbox').addClass('visible'); - } - } - - service.confirmAccessControlUpdate = function (callback) { - service.confirm({ - 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', - }, - }, - callback: callback, - }); - }; - - service.confirmImageForceRemoval = function (callback) { - service.confirm({ - title: 'Are you sure?', - 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: { - confirm: { - label: 'Remove the image', - className: 'btn-danger', - }, - }, - callback: callback, - }); - }; - - service.cancelRegistryRepositoryAction = function (callback) { - service.confirm({ - title: 'Are you sure?', - 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: { - confirm: { - label: 'Stop', - className: 'btn-danger', - }, - }, - callback: callback, - }); - }; - - service.confirmDeletion = function (message, callback) { - message = $sanitize(message); - service.confirm({ - title: 'Are you sure ?', - message: message, - buttons: { - confirm: { - label: 'Remove', - className: 'btn-danger', - }, - }, - callback: callback, - }); - }; - - service.confirmDeassociate = function (callback) { - const message = - 'De-associating this Edge environment will mark it as non associated and will clear the registered Edge ID.
' + - '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.
'; - service.confirm({ - title: 'About de-associating', - message: $sanitize(message), - buttons: { - confirm: { - label: 'De-associate', - className: 'btn-primary', - }, - }, - callback: callback, - }); - }; - - service.confirmUpdate = function (message, callback) { - message = $sanitize(message); - service.confirm({ - title: 'Are you sure ?', - message: message, - buttons: { - confirm: { - label: 'Update', - className: 'btn-warning', - }, - }, - callback: callback, - }); - }; - - service.confirmRedeploy = function (message, callback) { - message = $sanitize(message); - service.confirm({ - title: '', - message: message, - buttons: { - confirm: { - label: 'Redeploy the applications', - className: 'btn-primary', - }, - cancel: { - label: "I'll do it later", - }, - }, - callback: callback, - }); - }; - - service.confirmDeletionAsync = function confirmDeletionAsync(message) { - return new Promise((resolve) => { - service.confirmDeletion(message, (confirmed) => resolve(confirmed)); - }); - }; - - service.confirmContainerDeletion = function (title, callback) { - title = $sanitize(title); - prompt({ - title: title, - inputType: 'checkbox', - inputOptions: [ - { - text: 'Automatically remove non-persistent volumes', - value: '1', - }, - ], - buttons: { - confirm: { - label: 'Remove', - className: 'btn-danger', - }, - }, - callback: callback, - }); - }; - - service.confirmContainerRecreation = function (callback) { - customCheckboxPrompt({ - title: 'Are you sure?', - message: - "You're about to re-create this container, any non-persisted data will be lost. This container will be removed and another one will be created using the same configuration.", - inputOptions: [ - { - text: 'Pull latest image', - value: '1', - }, - ], - buttons: { - confirm: { - label: 'Recreate', - className: 'btn-danger', - }, - }, - callback: callback, - optionToggled: false, - }); - }; - - service.confirmEndpointSnapshot = function (callback) { - service.confirm({ - title: 'Are you sure?', - 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', - }, - }, - callback: callback, - }); - }; - - service.confirmImageExport = function (callback) { - service.confirm({ - title: 'Caution', - message: 'The export may take several minutes, do not navigate away whilst the export is in progress.', - buttons: { - confirm: { - label: 'Continue', - className: 'btn-primary', - }, - }, - callback: callback, - }); - }; - - service.confirmServiceForceUpdate = function (message, callback) { - message = $sanitize(message); - customCheckboxPrompt({ - title: 'Are you sure ?', - message: message, - inputOptions: [ - { - text: 'Pull latest image version', - value: '1', - }, - ], - buttons: { - confirm: { - label: 'Update', - className: 'btn-primary', - }, - }, - callback: callback, - optionToggled: false, - }); - }; - - service.selectRegistry = function (options) { - var box = bootbox.prompt({ - title: 'Which registry do you want to use?', - inputType: 'select', - value: options.defaultValue, - inputOptions: options.options, - callback: options.callback, - }); - applyBoxCSS(box); - }; - - service.confirmKubeconfigSelection = function (options, expiryMessage, callback) { - const message = 'Select the kubernetes environment(s) to add to the kubeconfig file.' + expiryMessage; - customCheckboxPrompt({ - title: 'Download kubeconfig file', - message: $sanitize(message), - inputOptions: options, - buttons: { - confirm: { - label: 'Download file', - className: 'btn-primary', - }, - }, - callback: callback, - optionToggled: true, - showCheck: true, - }); - }; - - return service; - }, -]); diff --git a/app/portainer/services/notifications.js b/app/portainer/services/notifications.js deleted file mode 100644 index e1135e67b..000000000 --- a/app/portainer/services/notifications.js +++ /dev/null @@ -1,59 +0,0 @@ -import _ from 'lodash-es'; -import toastr from 'toastr'; -import lodash from 'lodash-es'; - -angular.module('portainer.app').factory('Notifications', [ - '$sanitize', - function NotificationsFactory($sanitize) { - 'use strict'; - var service = {}; - - service.success = function (title, text) { - toastr.success($sanitize(_.escape(text)), $sanitize(title)); - }; - - service.warning = function (title, text) { - toastr.warning($sanitize(_.escape(text)), $sanitize(title), { timeOut: 6000 }); - }; - - function pickErrorMsg(e) { - const props = [ - 'err.data.details', - 'err.data.message', - 'data.details', - 'data.message', - 'data.content', - 'data.error', - 'message', - 'err.data[0].message', - 'err.data.err', - 'data.err', - 'msg', - ]; - - let msg = ''; - - lodash.forEach(props, (prop) => { - const val = lodash.get(e, prop); - if (typeof val === 'string') { - msg = msg || val; - } - }); - - return msg; - } - - service.error = function (title, e, fallbackText) { - const msg = pickErrorMsg(e) || fallbackText; - - // eslint-disable-next-line no-console - console.error(e); - - if (msg !== 'Invalid JWT token') { - toastr.error($sanitize(_.escape(msg)), $sanitize(title), { timeOut: 6000 }); - } - }; - - return service; - }, -]); diff --git a/app/portainer/services/notifications.test.ts b/app/portainer/services/notifications.test.ts new file mode 100644 index 000000000..693178338 --- /dev/null +++ b/app/portainer/services/notifications.test.ts @@ -0,0 +1,47 @@ +import toastr from 'toastr'; + +import { error, success, warning } from './notifications'; + +jest.mock('toastr'); + +it('calling success should show success message', () => { + const title = 'title'; + const text = 'text'; + + success(title, text); + + expect(toastr.success).toHaveBeenCalledWith(text, title); +}); + +it('calling error with Error should show error message', () => { + const title = 'title'; + const errorMessage = 'message'; + const fallback = 'fallback'; + + error(title, new Error(errorMessage), fallback); + + expect(toastr.error).toHaveBeenCalledWith( + errorMessage, + title, + expect.anything() + ); +}); + +it('calling error without Error should show fallback message', () => { + const title = 'title'; + + const fallback = 'fallback'; + + error(title, undefined, fallback); + + expect(toastr.error).toHaveBeenCalledWith(fallback, title, expect.anything()); +}); + +it('calling warning should show warning message', () => { + const title = 'title'; + const text = 'text'; + + warning(title, text); + + expect(toastr.warning).toHaveBeenCalledWith(text, title, expect.anything()); +}); diff --git a/app/portainer/services/notifications.ts b/app/portainer/services/notifications.ts new file mode 100644 index 000000000..77cdd4ec9 --- /dev/null +++ b/app/portainer/services/notifications.ts @@ -0,0 +1,69 @@ +import _ from 'lodash-es'; +import toastr from 'toastr'; +import sanitize from 'sanitize-html'; + +toastr.options = { + timeOut: 3000, + closeButton: true, + progressBar: true, + tapToDismiss: false, +}; + +export function success(title: string, text: string) { + toastr.success(sanitize(_.escape(text)), sanitize(title)); +} + +export function warning(title: string, text: string) { + toastr.warning(sanitize(_.escape(text)), sanitize(title), { timeOut: 6000 }); +} + +export function error(title: string, e?: Error, fallbackText = '') { + const msg = pickErrorMsg(e) || fallbackText; + + // eslint-disable-next-line no-console + console.error(e); + + if (msg !== 'Invalid JWT token') { + toastr.error(sanitize(_.escape(msg)), sanitize(title), { timeOut: 6000 }); + } +} + +/* @ngInject */ +export function Notifications() { + return { + success, + warning, + error, + }; +} + +function pickErrorMsg(e?: Error) { + if (!e) { + return ''; + } + + const props = [ + 'err.data.details', + 'err.data.message', + 'data.details', + 'data.message', + 'data.content', + 'data.error', + 'message', + 'err.data[0].message', + 'err.data.err', + 'data.err', + 'msg', + ]; + + let msg = ''; + + props.forEach((prop) => { + const val = _.get(e, prop); + if (typeof val === 'string') { + msg = msg || val; + } + }); + + return msg; +} diff --git a/app/portainer/services/registryModalService.js b/app/portainer/services/registryModalService.js index 6690cb771..04dbaa344 100644 --- a/app/portainer/services/registryModalService.js +++ b/app/portainer/services/registryModalService.js @@ -20,8 +20,8 @@ function ModalServiceFactory($q, ModalService, RegistryService) { const defaultValue = String(_.get(registryModel, 'Registry.Id', '0')); ModalService.selectRegistry({ - options, - defaultValue, + inputOptions: options, + value: defaultValue, callback: (registryId) => { if (registryId) { const registryModel = RegistryService.retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries, registryId); diff --git a/app/portainer/settings/authentication/ldap/ad-settings/ad-settings.controller.js b/app/portainer/settings/authentication/ldap/ad-settings/ad-settings.controller.js index 969388459..2a372de2f 100644 --- a/app/portainer/settings/authentication/ldap/ad-settings/ad-settings.controller.js +++ b/app/portainer/settings/authentication/ldap/ad-settings/ad-settings.controller.js @@ -1,5 +1,6 @@ import _ from 'lodash-es'; -import { HIDE_INTERNAL_AUTH } from '@/portainer/feature-flags/feature-ids'; + +import { FeatureId } from '@/portainer/feature-flags/enums'; export default class AdSettingsController { /* @ngInject */ @@ -8,7 +9,7 @@ export default class AdSettingsController { this.featureService = featureService; this.domainSuffix = ''; - this.limitedFeatureId = HIDE_INTERNAL_AUTH; + this.limitedFeatureId = FeatureId.HIDE_INTERNAL_AUTH; this.onTlscaCertChange = this.onTlscaCertChange.bind(this); this.searchUsers = this.searchUsers.bind(this); this.searchGroups = this.searchGroups.bind(this); diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-custom/ldap-settings-custom.controller.js b/app/portainer/settings/authentication/ldap/ldap-settings-custom/ldap-settings-custom.controller.js index 70c2ed83e..0ee280ccf 100644 --- a/app/portainer/settings/authentication/ldap/ldap-settings-custom/ldap-settings-custom.controller.js +++ b/app/portainer/settings/authentication/ldap/ldap-settings-custom/ldap-settings-custom.controller.js @@ -1,7 +1,8 @@ -import { EXTERNAL_AUTH_LDAP } from '@/portainer/feature-flags/feature-ids'; +import { FeatureId } from '@/portainer/feature-flags/enums'; + export default class LdapSettingsCustomController { constructor() { - this.limitedFeatureId = EXTERNAL_AUTH_LDAP; + this.limitedFeatureId = FeatureId.EXTERNAL_AUTH_LDAP; } addLDAPUrl() { diff --git a/app/portainer/settings/authentication/ldap/ldap-settings-openldap/ldap-settings-openldap.controller.js b/app/portainer/settings/authentication/ldap/ldap-settings-openldap/ldap-settings-openldap.controller.js index 5115548d1..249a13fe5 100644 --- a/app/portainer/settings/authentication/ldap/ldap-settings-openldap/ldap-settings-openldap.controller.js +++ b/app/portainer/settings/authentication/ldap/ldap-settings-openldap/ldap-settings-openldap.controller.js @@ -1,10 +1,10 @@ -import { EXTERNAL_AUTH_LDAP } from '@/portainer/feature-flags/feature-ids'; +import { FeatureId } from '@/portainer/feature-flags/enums'; export default class LdapSettingsOpenLDAPController { /* @ngInject */ constructor() { this.domainSuffix = ''; - this.limitedFeatureId = EXTERNAL_AUTH_LDAP; + this.limitedFeatureId = FeatureId.EXTERNAL_AUTH_LDAP; this.findDomainSuffix = this.findDomainSuffix.bind(this); this.parseDomainSuffix = this.parseDomainSuffix.bind(this); diff --git a/app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.controller.js b/app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.controller.js index b9a43d880..37ca91778 100644 --- a/app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.controller.js +++ b/app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.controller.js @@ -4,8 +4,8 @@ const SERVER_TYPES = { AD: 2, }; +import { FeatureId } from '@/portainer/feature-flags/enums'; import { buildLdapSettingsModel, buildOpenLDAPSettingsModel } from '@/portainer/settings/authentication/ldap/ldap-settings.model'; -import { EXTERNAL_AUTH_LDAP } from '@/portainer/feature-flags/feature-ids'; const DEFAULT_GROUP_FILTER = '(objectClass=groupOfNames)'; const DEFAULT_USER_FILTER = '(objectClass=inetOrgPerson)'; @@ -20,7 +20,7 @@ export default class LdapSettingsController { this.boxSelectorOptions = [ { id: 'ldap_custom', value: SERVER_TYPES.CUSTOM, label: 'Custom', icon: 'fa fa-server' }, - { id: 'ldap_openldap', value: SERVER_TYPES.OPEN_LDAP, label: 'OpenLDAP', icon: 'fa fa-server', feature: EXTERNAL_AUTH_LDAP }, + { id: 'ldap_openldap', value: SERVER_TYPES.OPEN_LDAP, label: 'OpenLDAP', icon: 'fa fa-server', feature: FeatureId.EXTERNAL_AUTH_LDAP }, ]; this.onTlscaCertChange = this.onTlscaCertChange.bind(this); diff --git a/app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.html b/app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.html index b1389f675..72e008858 100644 --- a/app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.html +++ b/app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.html @@ -12,8 +12,8 @@