diff --git a/app/constants.js b/app/constants.js deleted file mode 100644 index 3a824ce41..000000000 --- a/app/constants.js +++ /dev/null @@ -1,66 +0,0 @@ -import { BROWSER_OS_PLATFORM } from './react/constants'; - -export const API_ENDPOINT_AUTH = 'api/auth'; -export const API_ENDPOINT_BACKUP = 'api/backup'; -export const API_ENDPOINT_CUSTOM_TEMPLATES = 'api/custom_templates'; -export const API_ENDPOINT_EDGE_GROUPS = 'api/edge_groups'; -export const API_ENDPOINT_EDGE_JOBS = 'api/edge_jobs'; -export const API_ENDPOINT_EDGE_STACKS = 'api/edge_stacks'; -export const API_ENDPOINT_EDGE_TEMPLATES = 'api/edge_templates'; -export const API_ENDPOINT_ENDPOINTS = 'api/endpoints'; -export const API_ENDPOINT_ENDPOINT_GROUPS = 'api/endpoint_groups'; -export const API_ENDPOINT_KUBERNETES = 'api/kubernetes'; -export const API_ENDPOINT_MOTD = 'api/motd'; -export const API_ENDPOINT_REGISTRIES = 'api/registries'; -export const API_ENDPOINT_RESOURCE_CONTROLS = 'api/resource_controls'; -export const API_ENDPOINT_SETTINGS = 'api/settings'; -export const API_ENDPOINT_STACKS = 'api/stacks'; -export const API_ENDPOINT_SUPPORT = 'api/support'; -export const API_ENDPOINT_USERS = 'api/users'; -export const API_ENDPOINT_TAGS = 'api/tags'; -export const API_ENDPOINT_TEAMS = 'api/teams'; -export const API_ENDPOINT_TEAM_MEMBERSHIPS = 'api/team_memberships'; -export const API_ENDPOINT_TEMPLATES = 'api/templates'; -export const API_ENDPOINT_WEBHOOKS = 'api/webhooks'; -export const PAGINATION_MAX_ITEMS = 10; -export const APPLICATION_CACHE_VALIDITY = 3600; -export const CONSOLE_COMMANDS_LABEL_PREFIX = 'io.portainer.commands.'; -export const PREDEFINED_NETWORKS = ['host', 'bridge', 'ingress', 'nat', 'none']; -export const KUBERNETES_DEFAULT_NAMESPACE = 'default'; -export const KUBERNETES_SYSTEM_NAMESPACES = ['kube-system', 'kube-public', 'kube-node-lease', 'portainer']; -export const PORTAINER_FADEOUT = 1500; -export const STACK_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$'; -export const TEMPLATE_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$'; - -// don't declare new constants, either: -// - if only used in one file or module, declare in that file or module (as a regular js constant) -// - if needed across modules, declare like the above constant and use es6 import for that -angular - .module('portainer') - .constant('API_ENDPOINT_AUTH', API_ENDPOINT_AUTH) - .constant('API_ENDPOINT_BACKUP', API_ENDPOINT_BACKUP) - .constant('API_ENDPOINT_CUSTOM_TEMPLATES', API_ENDPOINT_CUSTOM_TEMPLATES) - .constant('API_ENDPOINT_EDGE_GROUPS', API_ENDPOINT_EDGE_GROUPS) - .constant('API_ENDPOINT_EDGE_JOBS', API_ENDPOINT_EDGE_JOBS) - .constant('API_ENDPOINT_EDGE_STACKS', API_ENDPOINT_EDGE_STACKS) - .constant('API_ENDPOINT_EDGE_TEMPLATES', API_ENDPOINT_EDGE_TEMPLATES) - .constant('API_ENDPOINT_ENDPOINTS', API_ENDPOINT_ENDPOINTS) - .constant('API_ENDPOINT_ENDPOINT_GROUPS', API_ENDPOINT_ENDPOINT_GROUPS) - .constant('API_ENDPOINT_KUBERNETES', API_ENDPOINT_KUBERNETES) - .constant('API_ENDPOINT_MOTD', API_ENDPOINT_MOTD) - .constant('API_ENDPOINT_REGISTRIES', API_ENDPOINT_REGISTRIES) - .constant('API_ENDPOINT_RESOURCE_CONTROLS', API_ENDPOINT_RESOURCE_CONTROLS) - .constant('API_ENDPOINT_SETTINGS', API_ENDPOINT_SETTINGS) - .constant('API_ENDPOINT_STACKS', API_ENDPOINT_STACKS) - .constant('API_ENDPOINT_SUPPORT', API_ENDPOINT_SUPPORT) - .constant('API_ENDPOINT_USERS', API_ENDPOINT_USERS) - .constant('API_ENDPOINT_TAGS', API_ENDPOINT_TAGS) - .constant('API_ENDPOINT_TEAMS', API_ENDPOINT_TEAMS) - .constant('API_ENDPOINT_TEAM_MEMBERSHIPS', API_ENDPOINT_TEAM_MEMBERSHIPS) - .constant('API_ENDPOINT_TEMPLATES', API_ENDPOINT_TEMPLATES) - .constant('API_ENDPOINT_WEBHOOKS', API_ENDPOINT_WEBHOOKS) - .constant('PAGINATION_MAX_ITEMS', PAGINATION_MAX_ITEMS) - .constant('APPLICATION_CACHE_VALIDITY', APPLICATION_CACHE_VALIDITY) - .constant('CONSOLE_COMMANDS_LABEL_PREFIX', CONSOLE_COMMANDS_LABEL_PREFIX) - .constant('PREDEFINED_NETWORKS', PREDEFINED_NETWORKS) - .constant('BROWSER_OS_PLATFORM', BROWSER_OS_PLATFORM); diff --git a/app/constants.ts b/app/constants.ts new file mode 100644 index 000000000..b09190cd6 --- /dev/null +++ b/app/constants.ts @@ -0,0 +1,30 @@ +// try to declare constant where it's needed, not in a global file +export const API_ENDPOINT_AUTH = 'api/auth'; +export const API_ENDPOINT_BACKUP = 'api/backup'; +export const API_ENDPOINT_CUSTOM_TEMPLATES = 'api/custom_templates'; +export const API_ENDPOINT_EDGE_GROUPS = 'api/edge_groups'; +export const API_ENDPOINT_EDGE_JOBS = 'api/edge_jobs'; +export const API_ENDPOINT_EDGE_STACKS = 'api/edge_stacks'; +export const API_ENDPOINT_EDGE_TEMPLATES = 'api/edge_templates'; +export const API_ENDPOINT_ENDPOINTS = 'api/endpoints'; +export const API_ENDPOINT_ENDPOINT_GROUPS = 'api/endpoint_groups'; +export const API_ENDPOINT_KUBERNETES = 'api/kubernetes'; +export const API_ENDPOINT_MOTD = 'api/motd'; +export const API_ENDPOINT_REGISTRIES = 'api/registries'; +export const API_ENDPOINT_RESOURCE_CONTROLS = 'api/resource_controls'; +export const API_ENDPOINT_SETTINGS = 'api/settings'; +export const API_ENDPOINT_STACKS = 'api/stacks'; +export const API_ENDPOINT_SUPPORT = 'api/support'; +export const API_ENDPOINT_USERS = 'api/users'; +export const API_ENDPOINT_TAGS = 'api/tags'; +export const API_ENDPOINT_TEAMS = 'api/teams'; +export const API_ENDPOINT_TEAM_MEMBERSHIPS = 'api/team_memberships'; +export const API_ENDPOINT_TEMPLATES = 'api/templates'; +export const API_ENDPOINT_WEBHOOKS = 'api/webhooks'; +export const PAGINATION_MAX_ITEMS = 10; +export const APPLICATION_CACHE_VALIDITY = 3600; +export const CONSOLE_COMMANDS_LABEL_PREFIX = 'io.portainer.commands.'; +export const PREDEFINED_NETWORKS = ['host', 'bridge', 'ingress', 'nat', 'none']; +export const PORTAINER_FADEOUT = 1500; +export const STACK_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$'; +export const TEMPLATE_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$'; diff --git a/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js index 2ee077516..da48c052a 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js +++ b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js @@ -16,8 +16,13 @@ class DockerComposeFormController { this.onChangeFormValues = this.onChangeFormValues.bind(this); } - onChangeFormValues(values) { - this.formValues = values; + onChangeFormValues(newValues) { + return this.$async(async () => { + this.formValues = { + ...this.formValues, + ...newValues, + }; + }); } onChangeMethod(method) { diff --git a/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.html b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.html index 9a600e6fd..189b75a56 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.html +++ b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.html @@ -21,7 +21,7 @@ You can upload a Compose file from your computer. - +
diff --git a/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.controller.js b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.controller.js index 821e6377d..64002c69c 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.controller.js +++ b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.controller.js @@ -39,7 +39,9 @@ class KubeManifestFormController { } onChangeMethod(method) { - this.state.Method = method; + return this.$async(async () => { + this.state.Method = method; + }); } } diff --git a/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.html b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.html index 06908e194..4da3d6db9 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.html +++ b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.html @@ -32,10 +32,4 @@ - + diff --git a/app/index.js b/app/index.js index 64dc29183..659e0086c 100644 --- a/app/index.js +++ b/app/index.js @@ -19,6 +19,7 @@ import './portainer/__module'; import { onStartupAngular } from './app'; import { configApp } from './config'; +import { constantsModule } from './ng-constants'; import { nomadModule } from './nomad'; @@ -52,6 +53,7 @@ angular 'moment-picker', 'angulartics', analyticsModule, + constantsModule, ]) .run(onStartupAngular) .config(configApp); diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index c5044ab27..16ffecfc7 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -63,11 +63,11 @@
Namespace
diff --git a/app/kubernetes/views/deploy/deploy.html b/app/kubernetes/views/deploy/deploy.html index 041745fff..50bd27ea8 100644 --- a/app/kubernetes/views/deploy/deploy.html +++ b/app/kubernetes/views/deploy/deploy.html @@ -13,7 +13,7 @@ Deploy
Namespace
-
+
@@ -78,14 +78,12 @@ @@ -161,7 +159,7 @@ -
-
-
-
-

Path is required.

-
-
-
-
- diff --git a/app/portainer/components/forms/git-form/git-form-additional-files-panel/git-form-additional-file-item/index.js b/app/portainer/components/forms/git-form/git-form-additional-files-panel/git-form-additional-file-item/index.js deleted file mode 100644 index f50efd346..000000000 --- a/app/portainer/components/forms/git-form/git-form-additional-files-panel/git-form-additional-file-item/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import controller from './git-form-additional-file-item.controller.js'; - -export const gitFormAdditionalFileItem = { - templateUrl: './git-form-additional-file-item.html', - controller, - - bindings: { - variable: '<', - index: '<', - - onChange: '<', - onRemove: '<', - }, -}; diff --git a/app/portainer/components/forms/git-form/git-form-additional-files-panel/git-form-additional-files-panel.controller.js b/app/portainer/components/forms/git-form/git-form-additional-files-panel/git-form-additional-files-panel.controller.js deleted file mode 100644 index 8f3d4c22b..000000000 --- a/app/portainer/components/forms/git-form/git-form-additional-files-panel/git-form-additional-files-panel.controller.js +++ /dev/null @@ -1,26 +0,0 @@ -class GitFormAutoUpdateFieldsetController { - /* @ngInject */ - constructor() { - this.add = this.add.bind(this); - this.onChangeVariable = this.onChangeVariable.bind(this); - } - - add() { - this.model.AdditionalFiles.push(''); - } - - onChangeVariable(index, variable) { - if (!variable) { - this.model.AdditionalFiles.splice(index, 1); - } else { - this.model.AdditionalFiles[index] = variable.value; - } - - this.onChange({ - ...this.model, - AdditionalFiles: this.model.AdditionalFiles, - }); - } -} - -export default GitFormAutoUpdateFieldsetController; diff --git a/app/portainer/components/forms/git-form/git-form-additional-files-panel/git-form-additional-files-panel.html b/app/portainer/components/forms/git-form/git-form-additional-files-panel/git-form-additional-files-panel.html deleted file mode 100644 index af8c38305..000000000 --- a/app/portainer/components/forms/git-form/git-form-additional-files-panel/git-form-additional-files-panel.html +++ /dev/null @@ -1,18 +0,0 @@ -
-
-
- -
-
- add file -
-
-
- -
-
diff --git a/app/portainer/components/forms/git-form/git-form-additional-files-panel/index.js b/app/portainer/components/forms/git-form/git-form-additional-files-panel/index.js deleted file mode 100644 index d6d525438..000000000 --- a/app/portainer/components/forms/git-form/git-form-additional-files-panel/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import controller from './git-form-additional-files-panel.controller.js'; - -export const gitFormAdditionalFilesPanel = { - templateUrl: './git-form-additional-files-panel.html', - controller, - bindings: { - model: '<', - onChange: '<', - }, -}; diff --git a/app/portainer/components/forms/git-form/git-form-auth-fieldset.controller.ts b/app/portainer/components/forms/git-form/git-form-auth-fieldset.controller.ts new file mode 100644 index 000000000..3b89e4b9a --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-auth-fieldset.controller.ts @@ -0,0 +1,97 @@ +import { IFormController } from 'angular'; +import { FormikErrors } from 'formik'; + +import { notifyError } from '@/portainer/services/notifications'; +import { IAuthenticationService } from '@/portainer/services/types'; +import { GitAuthModel } from '@/react/portainer/gitops/types'; +import { gitAuthValidation } from '@/react/portainer/gitops/AuthFieldset'; +import { getGitCredentials } from '@/portainer/views/account/git-credential/gitCredential.service'; +import { GitCredential } from '@/portainer/views/account/git-credential/types'; + +import { validateForm } from '@@/form-components/validate-form'; + +export default class GitFormAuthFieldsetController { + errors?: FormikErrors = {}; + + $async: (fn: () => Promise) => Promise; + + gitFormAuthFieldset?: IFormController; + + gitCredentials: Array = []; + + Authentication: IAuthenticationService; + + value?: GitAuthModel; + + onChange?: (value: GitAuthModel) => void; + + /* @ngInject */ + constructor( + $async: (fn: () => Promise) => Promise, + Authentication: IAuthenticationService + ) { + this.$async = $async; + this.Authentication = Authentication; + + this.handleChange = this.handleChange.bind(this); + this.runGitValidation = this.runGitValidation.bind(this); + } + + async handleChange(newValues: Partial) { + // this should never happen, but just in case + if (!this.value) { + throw new Error('GitFormController: value is required'); + } + + const value = { + ...this.value, + ...newValues, + }; + this.onChange?.(value); + await this.runGitValidation(value); + } + + async runGitValidation(value: GitAuthModel) { + return this.$async(async () => { + this.errors = {}; + this.gitFormAuthFieldset?.$setValidity( + 'gitFormAuth', + true, + this.gitFormAuthFieldset + ); + + this.errors = await validateForm( + () => gitAuthValidation(this.gitCredentials), + value + ); + if (this.errors && Object.keys(this.errors).length > 0) { + this.gitFormAuthFieldset?.$setValidity( + 'gitFormAuth', + false, + this.gitFormAuthFieldset + ); + } + }); + } + + async $onInit() { + try { + this.gitCredentials = await getGitCredentials( + this.Authentication.getUserDetails().ID + ); + } catch (err) { + notifyError( + 'Failure', + err as Error, + 'Unable to retrieve user saved git credentials' + ); + } + + // this should never happen, but just in case + if (!this.value) { + throw new Error('GitFormController: value is required'); + } + + await this.runGitValidation(this.value); + } +} diff --git a/app/portainer/components/forms/git-form/git-form-auth-fieldset.ts b/app/portainer/components/forms/git-form/git-form-auth-fieldset.ts new file mode 100644 index 000000000..c0713dae7 --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-auth-fieldset.ts @@ -0,0 +1,21 @@ +import { IComponentOptions } from 'angular'; + +import controller from './git-form-auth-fieldset.controller'; + +export const gitFormAuthFieldset: IComponentOptions = { + controller, + template: ` + + + +`, + bindings: { + value: '<', + onChange: '<', + isExplanationVisible: '<', + }, +}; diff --git a/app/portainer/components/forms/git-form/git-form-auth-fieldset/git-form-auth-fieldset.controller.js b/app/portainer/components/forms/git-form/git-form-auth-fieldset/git-form-auth-fieldset.controller.js deleted file mode 100644 index bf484e9b9..000000000 --- a/app/portainer/components/forms/git-form/git-form-auth-fieldset/git-form-auth-fieldset.controller.js +++ /dev/null @@ -1,55 +0,0 @@ -class GitFormComposeAuthFieldsetController { - /* @ngInject */ - constructor($scope) { - Object.assign(this, { $scope }); - - this.authValues = { - username: '', - password: '', - }; - - this.handleChange = this.handleChange.bind(this); - this.onChangeField = this.onChangeField.bind(this); - this.onChangeAuth = this.onChangeAuth.bind(this); - this.onChangeUsername = this.onChangeField('RepositoryUsername'); - this.onChangePassword = this.onChangeField('RepositoryPassword'); - } - - handleChange(...args) { - this.$scope.$evalAsync(() => { - this.onChange(...args); - }); - } - - onChangeField(field) { - return (value) => { - this.handleChange({ - ...this.model, - [field]: value, - }); - }; - } - - onChangeAuth(auth) { - if (!auth) { - this.authValues.username = this.model.RepositoryUsername; - this.authValues.password = this.model.RepositoryPassword; - } - - this.handleChange({ - ...this.model, - RepositoryAuthentication: auth, - RepositoryUsername: auth ? this.authValues.username : '', - RepositoryPassword: auth ? this.authValues.password : '', - }); - } - - $onInit() { - if (this.model.RepositoryAuthentication) { - this.authValues.username = this.model.RepositoryUsername; - this.authValues.password = this.model.RepositoryPassword; - } - } -} - -export default GitFormComposeAuthFieldsetController; diff --git a/app/portainer/components/forms/git-form/git-form-auth-fieldset/git-form-auth-fieldset.css b/app/portainer/components/forms/git-form/git-form-auth-fieldset/git-form-auth-fieldset.css deleted file mode 100644 index dcd209054..000000000 --- a/app/portainer/components/forms/git-form/git-form-auth-fieldset/git-form-auth-fieldset.css +++ /dev/null @@ -1,11 +0,0 @@ -.inline-label { - display: inline-block; - padding: 0 15px; - min-width: 200px; -} - -.inline-input { - display: inline-block; - margin-left: 15px; - width: calc(100% - 235px); -} diff --git a/app/portainer/components/forms/git-form/git-form-auth-fieldset/git-form-auth-fieldset.html b/app/portainer/components/forms/git-form/git-form-auth-fieldset/git-form-auth-fieldset.html deleted file mode 100644 index 875d56010..000000000 --- a/app/portainer/components/forms/git-form/git-form-auth-fieldset/git-form-auth-fieldset.html +++ /dev/null @@ -1,51 +0,0 @@ -
-
- -
-
-
- - Enabling authentication will store the credentials and it is advisable to use a git service account -
-
-
- -
- -
-
-
- -
- -
-
-
diff --git a/app/portainer/components/forms/git-form/git-form-auth-fieldset/index.js b/app/portainer/components/forms/git-form/git-form-auth-fieldset/index.js deleted file mode 100644 index 37fe43888..000000000 --- a/app/portainer/components/forms/git-form/git-form-auth-fieldset/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import controller from './git-form-auth-fieldset.controller.js'; -import './git-form-auth-fieldset.css'; - -export const gitFormAuthFieldset = { - templateUrl: './git-form-auth-fieldset.html', - controller, - bindings: { - model: '<', - onChange: '<', - showAuthExplanation: '<', - isEdit: '<', - }, -}; diff --git a/app/portainer/components/forms/git-form/git-form-auto-update-fieldset.controller.ts b/app/portainer/components/forms/git-form/git-form-auto-update-fieldset.controller.ts new file mode 100644 index 000000000..febb285d6 --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-auto-update-fieldset.controller.ts @@ -0,0 +1,80 @@ +import { IFormController } from 'angular'; +import { FormikErrors } from 'formik'; + +import { IAuthenticationService } from '@/portainer/services/types'; +import { AutoUpdateModel } from '@/react/portainer/gitops/types'; +import { autoUpdateValidation } from '@/react/portainer/gitops/AutoUpdateFieldset/validation'; + +import { validateForm } from '@@/form-components/validate-form'; + +export default class GitFormAutoUpdateFieldsetController { + errors?: FormikErrors = {}; + + $async: (fn: () => Promise) => Promise; + + gitFormAutoUpdate?: IFormController; + + Authentication: IAuthenticationService; + + value?: AutoUpdateModel; + + onChange?: (value: AutoUpdateModel) => void; + + /* @ngInject */ + constructor( + $async: (fn: () => Promise) => Promise, + Authentication: IAuthenticationService + ) { + this.$async = $async; + this.Authentication = Authentication; + + this.handleChange = this.handleChange.bind(this); + this.runGitValidation = this.runGitValidation.bind(this); + } + + async handleChange(newValues: Partial) { + // this should never happen, but just in case + if (!this.value) { + throw new Error('GitFormController: value is required'); + } + + const value = { + ...this.value, + ...newValues, + }; + this.onChange?.(value); + await this.runGitValidation(value); + } + + async runGitValidation(value: AutoUpdateModel) { + return this.$async(async () => { + this.errors = {}; + this.gitFormAutoUpdate?.$setValidity( + 'gitFormAuth', + true, + this.gitFormAutoUpdate + ); + + this.errors = await validateForm( + () => autoUpdateValidation(), + value + ); + if (this.errors && Object.keys(this.errors).length > 0) { + this.gitFormAutoUpdate?.$setValidity( + 'gitFormAuth', + false, + this.gitFormAutoUpdate + ); + } + }); + } + + async $onInit() { + // this should never happen, but just in case + if (!this.value) { + throw new Error('GitFormController: value is required'); + } + + await this.runGitValidation(this.value); + } +} diff --git a/app/portainer/components/forms/git-form/git-form-auto-update-fieldset.ts b/app/portainer/components/forms/git-form/git-form-auto-update-fieldset.ts new file mode 100644 index 000000000..a27e989cc --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-auto-update-fieldset.ts @@ -0,0 +1,24 @@ +import { IComponentOptions } from 'angular'; + +import controller from './git-form-auto-update-fieldset.controller'; + +export const gitFormAutoUpdate: IComponentOptions = { + template: ` + + + `, + bindings: { + value: '<', + onChange: '<', + environmentType: '@', + isForcePullVisible: '<', + baseWebhookUrl: '@', + }, + controller, +}; diff --git a/app/portainer/components/forms/git-form/git-form-auto-update-fieldset/git-form-auto-update-fieldset.controller.js b/app/portainer/components/forms/git-form/git-form-auto-update-fieldset/git-form-auto-update-fieldset.controller.js deleted file mode 100644 index 34dfe402b..000000000 --- a/app/portainer/components/forms/git-form/git-form-auto-update-fieldset/git-form-auto-update-fieldset.controller.js +++ /dev/null @@ -1,38 +0,0 @@ -import { FeatureId } from '@/react/portainer/feature-flags/enums'; - -class GitFormAutoUpdateFieldsetController { - /* @ngInject */ - constructor($scope, clipboard, StateManager) { - Object.assign(this, { $scope, clipboard, StateManager }); - - this.onChangeAutoUpdate = this.onChangeField('RepositoryAutomaticUpdates'); - this.onChangeMechanism = this.onChangeField('RepositoryMechanism'); - this.onChangeInterval = this.onChangeField('RepositoryFetchInterval'); - - this.limitedFeature = FeatureId.FORCE_REDEPLOYMENT; - this.stackPullImageFeature = FeatureId.STACK_PULL_IMAGE; - } - - copyWebhook() { - this.clipboard.copyText(this.model.RepositoryWebhookURL); - $('#copyNotification').show(); - $('#copyNotification').fadeOut(2000); - } - - onChangeField(field) { - return (value) => { - this.$scope.$evalAsync(() => { - this.onChange({ - ...this.model, - [field]: value, - }); - }); - }; - } - - $onInit() { - this.environmentType = this.StateManager.getState().endpoint.mode.provider; - } -} - -export default GitFormAutoUpdateFieldsetController; diff --git a/app/portainer/components/forms/git-form/git-form-auto-update-fieldset/git-form-auto-update-fieldset.html b/app/portainer/components/forms/git-form/git-form-auto-update-fieldset/git-form-auto-update-fieldset.html deleted file mode 100644 index 560427eac..000000000 --- a/app/portainer/components/forms/git-form/git-form-auto-update-fieldset/git-form-auto-update-fieldset.html +++ /dev/null @@ -1,122 +0,0 @@ - -
- - - When enabled, at each polling interval or webhook invocation, if the git repo differs from what was stored locally on the last git pull, the changes are deployed. -
-
-
- -
-
-
- - Any changes to this stack or application that have been made locally via Portainer or directly in the cluster will be overwritten by the git repository content, which may - cause service interruption. -
-
- -
-
-
- - -
-
-
-
- -
- -
- {{ $ctrl.model.RepositoryWebhookURL | truncatelr }} - - - - -
-
-
- -
- -
-
-
-
-

This field is required.

-

Please enter a valid time interval.

-

Minimum interval is 1m

-
-
-
-
-
-
- -
-
- -
-
- -
-
- -
-
-
diff --git a/app/portainer/components/forms/git-form/git-form-auto-update-fieldset/index.js b/app/portainer/components/forms/git-form/git-form-auto-update-fieldset/index.js deleted file mode 100644 index 6f510fb00..000000000 --- a/app/portainer/components/forms/git-form/git-form-auto-update-fieldset/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import controller from './git-form-auto-update-fieldset.controller.js'; - -export const gitFormAutoUpdateFieldset = { - templateUrl: './git-form-auto-update-fieldset.html', - controller, - bindings: { - model: '<', - onChange: '<', - showForcePullImage: '<', - }, -}; diff --git a/app/portainer/components/forms/git-form/git-form-compose-path-field/git-form-compose-path-field.html b/app/portainer/components/forms/git-form/git-form-compose-path-field/git-form-compose-path-field.html deleted file mode 100644 index 769e3c525..000000000 --- a/app/portainer/components/forms/git-form/git-form-compose-path-field/git-form-compose-path-field.html +++ /dev/null @@ -1,29 +0,0 @@ - -
- - - Indicate the path to the {{ $ctrl.deployMethod == 'compose' ? 'Compose' : 'Manifest' }} file from the root of your repository. - - To enable rebuilding of an image if already present on Docker standalone environments, includepull_policy: buildin your compose file as per - Docker documentation. - -
-
- -
- -
-
-
diff --git a/app/portainer/components/forms/git-form/git-form-compose-path-field/index.js b/app/portainer/components/forms/git-form/git-form-compose-path-field/index.js deleted file mode 100644 index 0073b6813..000000000 --- a/app/portainer/components/forms/git-form/git-form-compose-path-field/index.js +++ /dev/null @@ -1,9 +0,0 @@ -export const gitFormComposePathField = { - templateUrl: './git-form-compose-path-field.html', - bindings: { - deployMethod: '@', - value: '<', - onChange: '<', - isDockerStandalone: '<', - }, -}; diff --git a/app/portainer/components/forms/git-form/git-form-info-panel/git-form-info-panel.html b/app/portainer/components/forms/git-form/git-form-info-panel/git-form-info-panel.html deleted file mode 100644 index 716408f66..000000000 --- a/app/portainer/components/forms/git-form/git-form-info-panel/git-form-info-panel.html +++ /dev/null @@ -1,15 +0,0 @@ -
-
-

- This {{ $ctrl.type }} was deployed from the git repository {{ $ctrl.url }} - . -

-

- Update - {{ $ctrl.configFilePath }}, {{ $ctrl.additionalFiles.join(',') }} - in git and pull from here to update the {{ $ctrl.type }}. -

-
-
diff --git a/app/portainer/components/forms/git-form/git-form-info-panel/index.js b/app/portainer/components/forms/git-form/git-form-info-panel/index.js deleted file mode 100644 index 9cfe1c261..000000000 --- a/app/portainer/components/forms/git-form/git-form-info-panel/index.js +++ /dev/null @@ -1,10 +0,0 @@ -export const gitFormInfoPanel = { - templateUrl: './git-form-info-panel.html', - bindings: { - url: '<', - configFilePath: '<', - additionalFiles: '<', - className: '@', - type: '@', - }, -}; diff --git a/app/portainer/components/forms/git-form/git-form-ref-field.ts b/app/portainer/components/forms/git-form/git-form-ref-field.ts new file mode 100644 index 000000000..670c2d123 --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-ref-field.ts @@ -0,0 +1,83 @@ +import { IComponentOptions, IFormController } from 'angular'; + +import { GitFormModel } from '@/react/portainer/gitops/types'; +import { AsyncService } from '@/portainer/services/types'; +import { refFieldValidation } from '@/react/portainer/gitops/RefField/RefField'; + +import { validateForm } from '@@/form-components/validate-form'; + +class GitFormRefFieldController { + $async: AsyncService; + + value?: string; + + onChange?: (value: string) => void; + + gitFormRefField?: IFormController; + + error?: string = ''; + + model?: GitFormModel; + + stackId?: number = 0; + + /* @ngInject */ + constructor($async: AsyncService) { + this.$async = $async; + + this.handleChange = this.handleChange.bind(this); + this.runValidation = this.runValidation.bind(this); + } + + async handleChange(value: string) { + return this.$async(async () => { + this.onChange?.(value); + await this.runValidation(value); + }); + } + + async runValidation(value: string) { + return this.$async(async () => { + this.error = ''; + this.gitFormRefField?.$setValidity( + 'gitFormRefField', + true, + this.gitFormRefField + ); + + this.error = await validateForm( + () => refFieldValidation(), + value + ); + if (this.error) { + this.gitFormRefField?.$setValidity( + 'gitFormRefField', + false, + this.gitFormRefField + ); + } + }); + } +} + +export const gitFormRefField: IComponentOptions = { + controller: GitFormRefFieldController, + template: ` + + + +`, + bindings: { + isUrlValid: '<', + value: '<', + onChange: '<', + model: '<', + stackId: '<', + }, +}; diff --git a/app/portainer/components/forms/git-form/git-form-ref-field/git-form-ref-field.html b/app/portainer/components/forms/git-form/git-form-ref-field/git-form-ref-field.html deleted file mode 100644 index 0141b5227..000000000 --- a/app/portainer/components/forms/git-form/git-form-ref-field/git-form-ref-field.html +++ /dev/null @@ -1,23 +0,0 @@ -
- - - - Specify a reference of the repository using the following syntax: branches with refs/heads/branch_name or tags with refs/tags/tag_name. If not - specified, will use the default HEAD reference normally the master branch. - - -
-
- -
- -
-
diff --git a/app/portainer/components/forms/git-form/git-form-ref-field/index.js b/app/portainer/components/forms/git-form/git-form-ref-field/index.js deleted file mode 100644 index df8145061..000000000 --- a/app/portainer/components/forms/git-form/git-form-ref-field/index.js +++ /dev/null @@ -1,7 +0,0 @@ -export const gitFormRefField = { - templateUrl: './git-form-ref-field.html', - bindings: { - value: '<', - onChange: '<', - }, -}; diff --git a/app/portainer/components/forms/git-form/git-form-url-field/git-form-url-field.html b/app/portainer/components/forms/git-form/git-form-url-field/git-form-url-field.html deleted file mode 100644 index 6e4eaeeb6..000000000 --- a/app/portainer/components/forms/git-form/git-form-url-field/git-form-url-field.html +++ /dev/null @@ -1,19 +0,0 @@ -
- You can use the URL of a git repository. -
-
- -
- -
-
diff --git a/app/portainer/components/forms/git-form/git-form-url-field/index.js b/app/portainer/components/forms/git-form/git-form-url-field/index.js deleted file mode 100644 index 972df9aaa..000000000 --- a/app/portainer/components/forms/git-form/git-form-url-field/index.js +++ /dev/null @@ -1,7 +0,0 @@ -export const gitFormUrlField = { - templateUrl: './git-form-url-field.html', - bindings: { - value: '<', - onChange: '<', - }, -}; diff --git a/app/portainer/components/forms/git-form/git-form.controller.js b/app/portainer/components/forms/git-form/git-form.controller.js deleted file mode 100644 index 368a5ce53..000000000 --- a/app/portainer/components/forms/git-form/git-form.controller.js +++ /dev/null @@ -1,25 +0,0 @@ -export default class GitFormController { - /* @ngInject */ - constructor(StateManager) { - this.StateManager = StateManager; - - this.onChangeField = this.onChangeField.bind(this); - this.onChangeURL = this.onChangeField('RepositoryURL'); - this.onChangeRefName = this.onChangeField('RepositoryReferenceName'); - this.onChangeComposePath = this.onChangeField('ComposeFilePathInRepository'); - } - - onChangeField(field) { - return (value) => { - this.onChange({ - ...this.model, - [field]: value, - }); - }; - } - - $onInit() { - this.deployMethod = this.deployMethod || 'compose'; - this.isDockerStandalone = !this.hideRebuildInfo && this.StateManager.getState().endpoint.mode.provider === 'DOCKER_STANDALONE'; - } -} diff --git a/app/portainer/components/forms/git-form/git-form.controller.ts b/app/portainer/components/forms/git-form/git-form.controller.ts new file mode 100644 index 000000000..a30319412 --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form.controller.ts @@ -0,0 +1,87 @@ +import { IFormController } from 'angular'; +import { FormikErrors } from 'formik'; + +import { GitFormModel } from '@/react/portainer/gitops/types'; +import { validateGitForm } from '@/react/portainer/gitops/GitForm'; +import { notifyError } from '@/portainer/services/notifications'; +import { IAuthenticationService } from '@/portainer/services/types'; +import { getGitCredentials } from '@/portainer/views/account/git-credential/gitCredential.service'; +import { GitCredential } from '@/portainer/views/account/git-credential/types'; +import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; + +export default class GitFormController { + errors?: FormikErrors; + + $async: (fn: () => Promise) => Promise; + + gitForm?: IFormController; + + gitCredentials: Array = []; + + Authentication: IAuthenticationService; + + value?: GitFormModel; + + onChange?: (value: GitFormModel) => void; + + /* @ngInject */ + constructor( + $async: (fn: () => Promise) => Promise, + Authentication: IAuthenticationService + ) { + this.$async = $async; + this.Authentication = Authentication; + + this.handleChange = this.handleChange.bind(this); + this.runGitFormValidation = this.runGitFormValidation.bind(this); + } + + async handleChange(newValues: Partial) { + // this should never happen, but just in case + if (!this.value) { + throw new Error('GitFormController: value is required'); + } + + const value = { + ...this.value, + ...newValues, + }; + this.onChange?.(value); + await this.runGitFormValidation(value); + } + + async runGitFormValidation(value: GitFormModel) { + return this.$async(async () => { + this.errors = {}; + this.gitForm?.$setValidity('gitForm', true, this.gitForm); + + this.errors = await validateGitForm(this.gitCredentials, value); + if (this.errors && Object.keys(this.errors).length > 0) { + this.gitForm?.$setValidity('gitForm', false, this.gitForm); + } + }); + } + + async $onInit() { + if (isBE) { + try { + this.gitCredentials = await getGitCredentials( + this.Authentication.getUserDetails().ID + ); + } catch (err) { + notifyError( + 'Failure', + err as Error, + 'Unable to retrieve user saved git credentials' + ); + } + } + + // this should never happen, but just in case + if (!this.value) { + throw new Error('GitFormController: value is required'); + } + + await this.runGitFormValidation(this.value); + } +} diff --git a/app/portainer/components/forms/git-form/git-form.html b/app/portainer/components/forms/git-form/git-form.html deleted file mode 100644 index 9aba316f8..000000000 --- a/app/portainer/components/forms/git-form/git-form.html +++ /dev/null @@ -1,24 +0,0 @@ - -
Git repository
- - - - - - - - - - - -
diff --git a/app/portainer/components/forms/git-form/git-form.js b/app/portainer/components/forms/git-form/git-form.js deleted file mode 100644 index b5f4ce187..000000000 --- a/app/portainer/components/forms/git-form/git-form.js +++ /dev/null @@ -1,16 +0,0 @@ -import controller from './git-form.controller.js'; - -export const gitForm = { - templateUrl: './git-form.html', - controller, - bindings: { - deployMethod: '@', - model: '<', - onChange: '<', - additionalFile: '<', - autoUpdate: '<', - showAuthExplanation: '<', - showForcePullImage: '<', - hideRebuildInfo: '@', - }, -}; diff --git a/app/portainer/components/forms/git-form/git-form.ts b/app/portainer/components/forms/git-form/git-form.ts new file mode 100644 index 000000000..e1c38322f --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form.ts @@ -0,0 +1,33 @@ +import { IComponentOptions } from 'angular'; + +import controller from './git-form.controller'; + +export const gitForm: IComponentOptions = { + template: ` + + + +`, + bindings: { + value: '<', + onChange: '<', + isDockerStandalone: '<', + deployMethod: '@', + baseWebhookUrl: '@', + isAdditionalFilesFieldVisible: '<', + isAutoUpdateVisible: '<', + isForcePullVisible: '<', + isAuthExplanationVisible: '<', + }, + controller, +}; diff --git a/app/portainer/components/forms/git-form/index.js b/app/portainer/components/forms/git-form/index.js deleted file mode 100644 index e73966464..000000000 --- a/app/portainer/components/forms/git-form/index.js +++ /dev/null @@ -1,23 +0,0 @@ -import angular from 'angular'; - -import { gitForm } from './git-form'; -import { gitFormAuthFieldset } from './git-form-auth-fieldset'; -import { gitFormAdditionalFilesPanel } from './git-form-additional-files-panel'; -import { gitFormAdditionalFileItem } from './/git-form-additional-files-panel/git-form-additional-file-item'; -import { gitFormAutoUpdateFieldset } from './git-form-auto-update-fieldset'; -import { gitFormComposePathField } from './git-form-compose-path-field'; -import { gitFormRefField } from './git-form-ref-field'; -import { gitFormUrlField } from './git-form-url-field'; -import { gitFormInfoPanel } from './git-form-info-panel'; - -export default angular - .module('portainer.app.components.forms.git', []) - .component('gitFormComposePathField', gitFormComposePathField) - .component('gitFormRefField', gitFormRefField) - .component('gitForm', gitForm) - .component('gitFormUrlField', gitFormUrlField) - .component('gitFormInfoPanel', gitFormInfoPanel) - .component('gitFormAdditionalFilesPanel', gitFormAdditionalFilesPanel) - .component('gitFormAdditionalFileItem', gitFormAdditionalFileItem) - .component('gitFormAutoUpdateFieldset', gitFormAutoUpdateFieldset) - .component('gitFormAuthFieldset', gitFormAuthFieldset).name; diff --git a/app/portainer/components/forms/git-form/index.ts b/app/portainer/components/forms/git-form/index.ts new file mode 100644 index 000000000..e478b62ca --- /dev/null +++ b/app/portainer/components/forms/git-form/index.ts @@ -0,0 +1,13 @@ +import angular from 'angular'; + +import { gitForm } from './git-form'; +import { gitFormAuthFieldset } from './git-form-auth-fieldset'; +import { gitFormAutoUpdate } from './git-form-auto-update-fieldset'; +import { gitFormRefField } from './git-form-ref-field'; + +export const gitFormModule = angular + .module('portainer.app.components.git-form', []) + .component('gitForm', gitForm) + .component('gitFormAuthFieldset', gitFormAuthFieldset) + .component('gitFormAutoUpdateFieldset', gitFormAutoUpdate) + .component('gitFormRefField', gitFormRefField).name; diff --git a/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.controller.js b/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.controller.js deleted file mode 100644 index 813174654..000000000 --- a/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.controller.js +++ /dev/null @@ -1,95 +0,0 @@ -import { ModalType } from '@@/modals'; -import { confirm } from '@@/modals/confirm'; -import { buildConfirmButton } from '@@/modals/utils'; - -class KubernetesAppGitFormController { - /* @ngInject */ - constructor($async, $state, StackService, Notifications) { - this.$async = $async; - this.$state = $state; - this.StackService = StackService; - this.Notifications = Notifications; - - this.state = { - saveGitSettingsInProgress: false, - redeployInProgress: false, - showConfig: true, - isEdit: false, - }; - - this.formValues = { - RefName: '', - RepositoryAuthentication: false, - RepositoryUsername: '', - RepositoryPassword: '', - }; - - this.onChange = this.onChange.bind(this); - this.onChangeRef = this.onChangeRef.bind(this); - } - - onChangeRef(value) { - this.onChange({ RefName: value }); - } - - onChange(values) { - this.formValues = { - ...this.formValues, - ...values, - }; - } - - async pullAndRedeployApplication() { - return this.$async(async () => { - try { - const confirmed = await confirm({ - title: 'Are you sure?', - message: 'Any changes to this application will be overridden by the definition in git and may cause a service interruption. Do you wish to continue?', - confirmButton: buildConfirmButton('Update', 'warning'), - modalType: ModalType.Warn, - }); - if (!confirmed) { - return; - } - - this.state.redeployInProgress = true; - await this.StackService.updateKubeGit(this.stack.Id, this.stack.EndpointId, this.namespace, this.formValues); - this.Notifications.success('Success', 'Pulled and redeployed stack successfully'); - await this.$state.reload(this.$state.current); - } catch (err) { - this.Notifications.error('Failure', err, 'Failed redeploying application'); - } finally { - this.state.redeployInProgress = false; - } - }); - } - - async saveGitSettings() { - return this.$async(async () => { - try { - this.state.saveGitSettingsInProgress = true; - await this.StackService.updateKubeStack({ EndpointId: this.stack.EndpointId, Id: this.stack.Id }, null, this.formValues); - this.Notifications.success('Success', 'Save stack settings successfully'); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to save application settings'); - } finally { - this.state.saveGitSettingsInProgress = false; - } - }); - } - - isSubmitButtonDisabled() { - return this.state.saveGitSettingsInProgress || this.state.redeployInProgress; - } - - $onInit() { - this.formValues.RefName = this.stack.GitConfig.ReferenceName; - if (this.stack.GitConfig && this.stack.GitConfig.Authentication) { - this.formValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username; - this.formValues.RepositoryAuthentication = true; - this.state.isEdit = true; - } - } -} - -export default KubernetesAppGitFormController; diff --git a/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.html b/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.html deleted file mode 100644 index 468687645..000000000 --- a/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.html +++ /dev/null @@ -1,57 +0,0 @@ - -
Redeploy from git repository
-
-
-

Pull the latest manifest from git and redeploy the application.

-
-
- - - - - -
Actions
- - - - diff --git a/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.js b/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.js deleted file mode 100644 index 7a21a6384..000000000 --- a/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.js +++ /dev/null @@ -1,13 +0,0 @@ -import angular from 'angular'; -import controller from './kubernetes-app-git-form.controller'; - -const kubernetesAppGitForm = { - templateUrl: './kubernetes-app-git-form.html', - controller, - bindings: { - namespace: '<', - stack: '<', - }, -}; - -angular.module('portainer.app').component('kubernetesAppGitForm', kubernetesAppGitForm); diff --git a/app/portainer/components/forms/kubernetes-redeploy-app-git-form/kubernetes-redeploy-app-git-form.controller.js b/app/portainer/components/forms/kubernetes-redeploy-app-git-form/kubernetes-redeploy-app-git-form.controller.js index f65317a78..360b6cd42 100644 --- a/app/portainer/components/forms/kubernetes-redeploy-app-git-form/kubernetes-redeploy-app-git-form.controller.js +++ b/app/portainer/components/forms/kubernetes-redeploy-app-git-form/kubernetes-redeploy-app-git-form.controller.js @@ -1,16 +1,17 @@ -import uuidv4 from 'uuid/v4'; import { RepositoryMechanismTypes } from 'Kubernetes/models/deploy'; import { confirm } from '@@/modals/confirm'; import { buildConfirmButton } from '@@/modals/utils'; import { ModalType } from '@@/modals'; +import { parseAutoUpdateResponse } from '@/react/portainer/gitops/AutoUpdateFieldset/utils'; +import { baseStackWebhookUrl } from '@/portainer/helpers/webhookHelper'; + class KubernetesRedeployAppGitFormController { /* @ngInject */ - constructor($async, $state, StackService, Notifications, WebhookHelper) { + constructor($async, $state, StackService, Notifications) { this.$async = $async; this.$state = $state; this.StackService = StackService; this.Notifications = Notifications; - this.WebhookHelper = WebhookHelper; this.state = { saveGitSettingsInProgress: false, @@ -18,6 +19,7 @@ class KubernetesRedeployAppGitFormController { showConfig: false, isEdit: false, hasUnsavedChanges: false, + baseWebhookUrl: baseStackWebhookUrl(), }; this.formValues = { @@ -26,37 +28,44 @@ class KubernetesRedeployAppGitFormController { RepositoryUsername: '', RepositoryPassword: '', // auto update - AutoUpdate: { - RepositoryAutomaticUpdates: false, - RepositoryMechanism: RepositoryMechanismTypes.INTERVAL, - RepositoryFetchInterval: '5m', - RepositoryWebhookURL: '', - }, + AutoUpdate: parseAutoUpdateResponse(), }; this.onChange = this.onChange.bind(this); this.onChangeRef = this.onChangeRef.bind(this); this.onChangeAutoUpdate = this.onChangeAutoUpdate.bind(this); + this.onChangeGitAuth = this.onChangeGitAuth.bind(this); } onChangeRef(value) { this.onChange({ RefName: value }); } - onChange(values) { - this.formValues = { - ...this.formValues, - ...values, - }; - this.state.hasUnsavedChanges = angular.toJson(this.savedFormValues) !== angular.toJson(this.formValues); + async onChange(values) { + return this.$async(async () => { + this.formValues = { + ...this.formValues, + ...values, + }; + + this.state.hasUnsavedChanges = angular.toJson(this.savedFormValues) !== angular.toJson(this.formValues); + }); } - onChangeAutoUpdate(values) { - this.onChange({ - AutoUpdate: { - ...this.formValues.AutoUpdate, - ...values, - }, + onChangeGitAuth(values) { + return this.$async(async () => { + this.onChange(values); + }); + } + + async onChangeAutoUpdate(values) { + return this.$async(async () => { + await this.onChange({ + AutoUpdate: { + ...this.formValues.AutoUpdate, + ...values, + }, + }); }); } @@ -126,22 +135,8 @@ class KubernetesRedeployAppGitFormController { $onInit() { this.formValues.RefName = this.stack.GitConfig.ReferenceName; - // Init auto update - if (this.stack.AutoUpdate && (this.stack.AutoUpdate.Interval || this.stack.AutoUpdate.Webhook)) { - this.formValues.AutoUpdate.RepositoryAutomaticUpdates = true; - - if (this.stack.AutoUpdate.Interval) { - this.formValues.AutoUpdate.RepositoryMechanism = RepositoryMechanismTypes.INTERVAL; - this.formValues.AutoUpdate.RepositoryFetchInterval = this.stack.AutoUpdate.Interval; - } else if (this.stack.AutoUpdate.Webhook) { - this.formValues.AutoUpdate.RepositoryMechanism = RepositoryMechanismTypes.WEBHOOK; - this.formValues.AutoUpdate.RepositoryWebhookURL = this.WebhookHelper.returnStackWebhookUrl(this.stack.AutoUpdate.Webhook); - } - } - if (!this.formValues.AutoUpdate.RepositoryWebhookURL) { - this.formValues.AutoUpdate.RepositoryWebhookURL = this.WebhookHelper.returnStackWebhookUrl(uuidv4()); - } + this.formValues.AutoUpdate = parseAutoUpdateResponse(this.stack.AutoUpdate); if (this.stack.GitConfig && this.stack.GitConfig.Authentication) { this.formValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username; diff --git a/app/portainer/components/forms/kubernetes-redeploy-app-git-form/kubernetes-redeploy-app-git-form.html b/app/portainer/components/forms/kubernetes-redeploy-app-git-form/kubernetes-redeploy-app-git-form.html index 3d51487a0..da5de262a 100644 --- a/app/portainer/components/forms/kubernetes-redeploy-app-git-form/kubernetes-redeploy-app-git-form.html +++ b/app/portainer/components/forms/kubernetes-redeploy-app-git-form/kubernetes-redeploy-app-git-form.html @@ -5,7 +5,14 @@

Pull the latest manifest from git and redeploy the application.

- + + +

@@ -17,14 +24,14 @@

- - + is-url-valid="true" + >
Actions
diff --git a/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.controller.js b/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.controller.js index ce156a5ce..1fe0bd2d6 100644 --- a/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.controller.js +++ b/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.controller.js @@ -1,18 +1,19 @@ -import uuidv4 from 'uuid/v4'; import { RepositoryMechanismTypes } from 'Kubernetes/models/deploy'; import { FeatureId } from '@/react/portainer/feature-flags/enums'; import { confirmStackUpdate } from '@/react/docker/stacks/common/confirm-stack-update'; +import { parseAutoUpdateResponse } from '@/react/portainer/gitops/AutoUpdateFieldset/utils'; +import { baseStackWebhookUrl } from '@/portainer/helpers/webhookHelper'; + class StackRedeployGitFormController { /* @ngInject */ - constructor($async, $state, $compile, $scope, StackService, Notifications, WebhookHelper, FormHelper) { + constructor($async, $state, $compile, $scope, StackService, ModalService, Notifications, FormHelper) { this.$async = $async; this.$state = $state; this.$compile = $compile; this.$scope = $scope; this.StackService = StackService; this.Notifications = Notifications; - this.WebhookHelper = WebhookHelper; this.FormHelper = FormHelper; $scope.stackPullImageFeature = FeatureId.STACK_PULL_IMAGE; this.state = { @@ -21,6 +22,7 @@ class StackRedeployGitFormController { showConfig: false, isEdit: false, hasUnsavedChanges: false, + baseWebhookUrl: baseStackWebhookUrl(), }; this.formValues = { @@ -34,12 +36,7 @@ class StackRedeployGitFormController { Prune: false, }, // auto update - AutoUpdate: { - RepositoryAutomaticUpdates: false, - RepositoryMechanism: RepositoryMechanismTypes.INTERVAL, - RepositoryFetchInterval: '5m', - RepositoryWebhookURL: '', - }, + AutoUpdate: parseAutoUpdateResponse(), }; this.onChange = this.onChange.bind(this); @@ -47,6 +44,7 @@ class StackRedeployGitFormController { this.onChangeAutoUpdate = this.onChangeAutoUpdate.bind(this); this.onChangeEnvVar = this.onChangeEnvVar.bind(this); this.onChangeOption = this.onChangeOption.bind(this); + this.onChangeGitAuth = this.onChangeGitAuth.bind(this); } buildAnalyticsProperties() { @@ -69,27 +67,19 @@ class StackRedeployGitFormController { } onChange(values) { - this.formValues = { - ...this.formValues, - ...values, - }; - - this.state.hasUnsavedChanges = angular.toJson(this.savedFormValues) !== angular.toJson(this.formValues); + return this.$async(async () => { + this.formValues = { + ...this.formValues, + ...values, + }; + this.state.hasUnsavedChanges = angular.toJson(this.savedFormValues) !== angular.toJson(this.formValues); + }); } onChangeRef(value) { this.onChange({ RefName: value }); } - onChangeAutoUpdate(values) { - this.onChange({ - AutoUpdate: { - ...this.formValues.AutoUpdate, - ...values, - }, - }); - } - onChangeEnvVar(value) { this.onChange({ Env: value }); } @@ -167,29 +157,28 @@ class StackRedeployGitFormController { return isEnabled !== wasEnabled; } - $onInit() { + onChangeGitAuth(values) { + this.onChange(values); + } + + onChangeAutoUpdate(values) { + this.onChange({ + AutoUpdate: { + ...this.formValues.AutoUpdate, + ...values, + }, + }); + } + + async $onInit() { this.formValues.RefName = this.model.ReferenceName; this.formValues.Env = this.stack.Env; + if (this.stack.Option) { this.formValues.Option = this.stack.Option; } - // Init auto update - if (this.stack.AutoUpdate && (this.stack.AutoUpdate.Interval || this.stack.AutoUpdate.Webhook)) { - this.formValues.AutoUpdate.RepositoryAutomaticUpdates = true; - - if (this.stack.AutoUpdate.Interval) { - this.formValues.AutoUpdate.RepositoryMechanism = RepositoryMechanismTypes.INTERVAL; - this.formValues.AutoUpdate.RepositoryFetchInterval = this.stack.AutoUpdate.Interval; - } else if (this.stack.AutoUpdate.Webhook) { - this.formValues.AutoUpdate.RepositoryMechanism = RepositoryMechanismTypes.WEBHOOK; - this.formValues.AutoUpdate.RepositoryWebhookURL = this.WebhookHelper.returnStackWebhookUrl(this.stack.AutoUpdate.Webhook); - } - } - - if (!this.formValues.AutoUpdate.RepositoryWebhookURL) { - this.formValues.AutoUpdate.RepositoryWebhookURL = this.WebhookHelper.returnStackWebhookUrl(uuidv4()); - } + this.formValues.AutoUpdate = parseAutoUpdateResponse(this.stack.AutoUpdate); if (this.stack.GitConfig && this.stack.GitConfig.Authentication) { this.formValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username; diff --git a/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.html b/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.html index 3567c34c4..13a878776 100644 --- a/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.html +++ b/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.html @@ -1,17 +1,19 @@
Redeploy from git repository
@@ -24,14 +26,17 @@

- - + is-url-valid="true" + stack-id="$ctrl.gitStackId" + > + + + +
Actions
+ ) { return ( - + ); } diff --git a/app/react/components/form-components/FormControl/FormControl.tsx b/app/react/components/form-components/FormControl/FormControl.tsx index ae71c283c..017f627e1 100644 --- a/app/react/components/form-components/FormControl/FormControl.tsx +++ b/app/react/components/form-components/FormControl/FormControl.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren, ReactNode } from 'react'; +import { ComponentProps, PropsWithChildren, ReactNode } from 'react'; import clsx from 'clsx'; import { Tooltip } from '@@/Tip/Tooltip'; @@ -11,11 +11,12 @@ export interface Props { inputId?: string; label: ReactNode; size?: Size; - tooltip?: string; + tooltip?: ComponentProps['message']; + setTooltipHtmlMessage?: ComponentProps['setHtmlMessage']; children: ReactNode; errors?: ReactNode; required?: boolean; - setTooltipHtmlMessage?: boolean; + className?: string; } export function FormControl({ @@ -25,12 +26,14 @@ export function FormControl({ tooltip = '', children, errors, - required, + className, + required = false, setTooltipHtmlMessage, }: PropsWithChildren) { return (
{children} - - {errors && ( - - {errors} - - )} + {errors && {errors}}
); diff --git a/app/react/components/form-components/FormError.tsx b/app/react/components/form-components/FormError.tsx index 97e1f2cc6..8b96a89e5 100644 --- a/app/react/components/form-components/FormError.tsx +++ b/app/react/components/form-components/FormError.tsx @@ -10,7 +10,9 @@ interface Props { export function FormError({ children, className }: PropsWithChildren) { return ( -

+

{children}

diff --git a/app/react/components/form-components/Input/Input.tsx b/app/react/components/form-components/Input/Input.tsx index 0a3aded0c..c1e087625 100644 --- a/app/react/components/form-components/Input/Input.tsx +++ b/app/react/components/form-components/Input/Input.tsx @@ -1,14 +1,24 @@ import clsx from 'clsx'; -import { InputHTMLAttributes } from 'react'; +import { forwardRef, InputHTMLAttributes, Ref } from 'react'; + +export const InputWithRef = forwardRef< + HTMLInputElement, + InputHTMLAttributes +>( + // eslint-disable-next-line react/jsx-props-no-spreading + (props, ref) => +); export function Input({ className, + mRef: ref, ...props -}: InputHTMLAttributes) { +}: InputHTMLAttributes & { mRef?: Ref }) { return ( ); diff --git a/app/react/components/form-components/InputList/index.ts b/app/react/components/form-components/InputList/index.ts index 6d613f13a..8be0fcfc8 100644 --- a/app/react/components/form-components/InputList/index.ts +++ b/app/react/components/form-components/InputList/index.ts @@ -1 +1 @@ -export { InputList } from './InputList'; +export { InputList, type ItemProps } from './InputList'; diff --git a/app/react/components/form-components/SwitchField/SwitchField.tsx b/app/react/components/form-components/SwitchField/SwitchField.tsx index b2e958a8a..c65276dff 100644 --- a/app/react/components/form-components/SwitchField/SwitchField.tsx +++ b/app/react/components/form-components/SwitchField/SwitchField.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx'; +import { ComponentProps } from 'react'; import { FeatureId } from '@/react/portainer/feature-flags/enums'; @@ -13,14 +14,14 @@ export interface Props { onChange(value: boolean): void; name?: string; - tooltip?: string; + tooltip?: ComponentProps['message']; + setTooltipHtmlMessage?: ComponentProps['setHtmlMessage']; labelClass?: string; switchClass?: string; fieldClass?: string; dataCy?: string; disabled?: boolean; featureId?: FeatureId; - setTooltipHtmlMessage?: boolean; } export function SwitchField({ diff --git a/app/react/components/form-components/useCachedTest.ts b/app/react/components/form-components/useCachedTest.ts new file mode 100644 index 000000000..14dfbfc16 --- /dev/null +++ b/app/react/components/form-components/useCachedTest.ts @@ -0,0 +1,27 @@ +import { useRef } from 'react'; +import { TestContext, TestFunction } from 'yup'; + +function cacheTest( + asyncValidate: TestFunction +): TestFunction { + let valid = true; + let value: T | undefined; + + return async (newValue: T, context: TestContext) => { + if (newValue !== value) { + value = newValue; + + const response = await asyncValidate.call(context, newValue, context); + valid = !!response; + } + return valid; + }; +} + +export function useCachedValidation( + test: TestFunction +) { + const ref = useRef(cacheTest(test)); + + return ref.current; +} diff --git a/app/react/components/form-components/useCaretPosition.ts b/app/react/components/form-components/useCaretPosition.ts new file mode 100644 index 000000000..cf0eb9146 --- /dev/null +++ b/app/react/components/form-components/useCaretPosition.ts @@ -0,0 +1,26 @@ +import { useRef, useState, useCallback, useEffect } from 'react'; + +export function useCaretPosition< + T extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement +>() { + const node = useRef(null); + const [start, setStart] = useState(0); + const [end, setEnd] = useState(0); + + const updateCaret = useCallback(() => { + if (node.current) { + const { selectionStart, selectionEnd } = node.current; + + setStart(selectionStart || 0); + setEnd(selectionEnd || 0); + } + }, []); + + useEffect(() => { + if (node.current) { + node.current.setSelectionRange(start, end); + } + }); + + return { start, end, ref: node, updateCaret }; +} diff --git a/app/react/components/form-components/validate-form.ts b/app/react/components/form-components/validate-form.ts new file mode 100644 index 000000000..0d3e54450 --- /dev/null +++ b/app/react/components/form-components/validate-form.ts @@ -0,0 +1,19 @@ +import { yupToFormErrors } from 'formik'; +import { SchemaOf } from 'yup'; + +export async function validateForm( + schemaBuilder: () => SchemaOf, + formValues: T +) { + const validationSchema = schemaBuilder(); + + try { + await validationSchema.validate(formValues, { + strict: true, + abortEarly: false, + }); + return undefined; + } catch (error) { + return yupToFormErrors(error); + } +} diff --git a/app/react/docker/stacks/types.ts b/app/react/docker/stacks/types.ts index 62c2246f7..062028962 100644 --- a/app/react/docker/stacks/types.ts +++ b/app/react/docker/stacks/types.ts @@ -1,3 +1,5 @@ +export type StackId = number; + export enum StackType { /** * Represents a stack managed via docker stack diff --git a/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx b/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx index 6e4fc756e..7b0a8c57f 100644 --- a/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx +++ b/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx @@ -58,7 +58,9 @@ export function ScriptTabs({ children: ( <> {cmd} - Copy +
+ Copy +
), }; diff --git a/app/react/hooks/useCurrentEnvironment.ts b/app/react/hooks/useCurrentEnvironment.ts index 70a2c9ada..7beaf6fb6 100644 --- a/app/react/hooks/useCurrentEnvironment.ts +++ b/app/react/hooks/useCurrentEnvironment.ts @@ -1,8 +1,8 @@ -import { useEnvironment } from '@/react/portainer/environments/queries/useEnvironment'; +import { useEnvironment } from '@/react/portainer/environments/queries'; import { useEnvironmentId } from './useEnvironmentId'; -export function useCurrentEnvironment() { - const id = useEnvironmentId(); +export function useCurrentEnvironment(force = true) { + const id = useEnvironmentId(force); return useEnvironment(id); } diff --git a/app/react/hooks/useDebounce.ts b/app/react/hooks/useDebounce.ts index 24c9c5535..3e413417e 100644 --- a/app/react/hooks/useDebounce.ts +++ b/app/react/hooks/useDebounce.ts @@ -1,21 +1,22 @@ import _ from 'lodash'; -import { useState, useRef, useCallback } from 'react'; +import { useState, useRef, useCallback, useEffect } from 'react'; -export function useDebounce( - defaultValue: string, - onChange: (value: string) => void -) { - const [searchValue, setSearchValue] = useState(defaultValue); +export function useDebounce(value: string, onChange: (value: string) => void) { + const [debouncedValue, setDebouncedValue] = useState(value); const onChangeDebounces = useRef(_.debounce(onChange, 300)); const handleChange = useCallback( (value: string) => { - setSearchValue(value); + setDebouncedValue(value); onChangeDebounces.current(value); }, - [onChangeDebounces, setSearchValue] + [onChangeDebounces, setDebouncedValue] ); - return [searchValue, handleChange] as const; + useEffect(() => { + setDebouncedValue(value); + }, [value]); + + return [debouncedValue, handleChange] as const; } diff --git a/app/react/hooks/useEnvironmentId.ts b/app/react/hooks/useEnvironmentId.ts index c3fdd1c4b..7ba1667b8 100644 --- a/app/react/hooks/useEnvironmentId.ts +++ b/app/react/hooks/useEnvironmentId.ts @@ -1,13 +1,19 @@ import { useCurrentStateAndParams } from '@uirouter/react'; -export function useEnvironmentId() { +import { EnvironmentId } from '@/react/portainer/environments/types'; + +export function useEnvironmentId(force = true): EnvironmentId { const { params: { endpointId: environmentId }, } = useCurrentStateAndParams(); if (!environmentId) { + if (!force) { + return 0; + } + throw new Error('endpointId url param is required'); } - return environmentId; + return parseInt(environmentId, 10); } diff --git a/app/react/portainer/account/CreateAccessTokenView/CreateAccessToken.tsx b/app/react/portainer/account/CreateAccessTokenView/CreateAccessToken.tsx index f99cae8a1..61a454242 100644 --- a/app/react/portainer/account/CreateAccessTokenView/CreateAccessToken.tsx +++ b/app/react/portainer/account/CreateAccessTokenView/CreateAccessToken.tsx @@ -94,9 +94,11 @@ export function CreateAccessToken({ {accessToken} - - Copy access token - +
+ + Copy access token + +