diff --git a/.vscode.example/portainer.code-snippets b/.vscode.example/portainer.code-snippets index fefb732ce..fa3511098 100644 --- a/.vscode.example/portainer.code-snippets +++ b/.vscode.example/portainer.code-snippets @@ -163,5 +163,19 @@ "// @failure 500 \"Server error\"", "// @router /{id} [get]" ] + }, + "analytics": { + "prefix": "nlt", + "body": ["analytics-on", "analytics-category=\"$1\"", "analytics-event=\"$2\""], + "description": "analytics" + }, + "analytics-if": { + "prefix": "nltf", + "body": ["analytics-if=\"$1\""], + "description": "analytics" + }, + "analytics-metadata": { + "prefix": "nltm", + "body": "analytics-properties=\"{ metadata: { $1 } }\"" } } diff --git a/app/angulartics.matomo/index.js b/app/angulartics.matomo/index.js index d9477ad1f..f6a4e528e 100644 --- a/app/angulartics.matomo/index.js +++ b/app/angulartics.matomo/index.js @@ -1,4 +1,5 @@ import angular from 'angular'; +import _ from 'lodash-es'; const basePath = 'http://portainer-ce.app'; @@ -131,7 +132,8 @@ function config($analyticsProvider, $windowProvider) { let metadataString = ''; if (metadata) { - metadataString = JSON.stringify(metadata).toLowerCase(); + const kebabCasedMetadata = Object.fromEntries(Object.entries(metadata).map(([key, value]) => [_.kebabCase(key), value])); + metadataString = JSON.stringify(kebabCasedMetadata).toLowerCase(); } push([ diff --git a/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html b/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html index c135cca0d..916bcc1a7 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html +++ b/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html @@ -199,6 +199,10 @@ ng-click="$ctrl.createStack()" button-spinner="$ctrl.state.actionInProgress" data-cy="edgeStackCreate-createStackButton" + analytics-on + analytics-event="edge-stack-creation" + analytics-category="edge" + analytics-properties="$ctrl.buildAnalyticsProperties()" > Deploy the stack Deployment in progress... diff --git a/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackViewController.js b/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackViewController.js index bccfeddac..a9b229aa4 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackViewController.js +++ b/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackViewController.js @@ -43,6 +43,30 @@ export class CreateEdgeStackViewController { this.onChangeFormValues = this.onChangeFormValues.bind(this); } + buildAnalyticsProperties() { + const format = 'compose'; + const metadata = { type: methodLabel(this.state.Method), format }; + + if (metadata.type === 'template') { + metadata.templateName = this.selectedTemplate.title; + } + + return { metadata }; + + function methodLabel(method) { + switch (method) { + case 'editor': + return 'web-editor'; + case 'repository': + return 'git'; + case 'upload': + return 'file-upload'; + case 'template': + return 'template'; + } + } + } + async uiCanExit() { if (this.state.Method === 'editor' && this.formValues.StackFileContent && this.state.isEditorDirty) { return this.ModalService.confirmWebEditorDiscard(); diff --git a/app/kubernetes/components/kubectl-shell/kubectl-shell.html b/app/kubernetes/components/kubectl-shell/kubectl-shell.html index 76dae9afe..a17c86533 100644 --- a/app/kubernetes/components/kubectl-shell/kubectl-shell.html +++ b/app/kubernetes/components/kubectl-shell/kubectl-shell.html @@ -1,4 +1,13 @@ - + kubectl shell diff --git a/app/kubernetes/views/configure/configure.html b/app/kubernetes/views/configure/configure.html index 48f2a90dd..9ea2f7cf5 100644 --- a/app/kubernetes/views/configure/configure.html +++ b/app/kubernetes/views/configure/configure.html @@ -299,6 +299,11 @@ ng-click="ctrl.configure()" ng-disabled="ctrl.state.actionInProgress || !kubernetesClusterSetupForm.$valid || !ctrl.hasValidStorageConfiguration()" button-spinner="ctrl.state.actionInProgress" + analytics-on + analytics-if="ctrl.restrictDefaultToggledOn()" + analytics-category="kubernetes" + analytics-event="kubernetes-configure" + analytics-properties="{ metadata: { restrictAccessToDefaultNamespace: ctrl.formValues.RestrictDefaultNamespace } }" > Save configuration Saving configuration... diff --git a/app/kubernetes/views/configure/configureController.js b/app/kubernetes/views/configure/configureController.js index 9213ae4e4..8e41a3848 100644 --- a/app/kubernetes/views/configure/configureController.js +++ b/app/kubernetes/views/configure/configureController.js @@ -237,6 +237,10 @@ class KubernetesConfigureController { } /* #endregion */ + restrictDefaultToggledOn() { + return this.formValues.RestrictDefaultNamespace && !this.oldFormValues.RestrictDefaultNamespace; + } + /* #region ON INIT */ async onInit() { this.state = { @@ -287,6 +291,8 @@ class KubernetesConfigureController { ic.NeedsDeletion = false; return ic; }); + + this.oldFormValues = Object.assign({}, this.formValues); } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve endpoint configuration'); } finally { diff --git a/app/kubernetes/views/deploy/deploy.html b/app/kubernetes/views/deploy/deploy.html index dd7b61228..e0ed3d9e9 100644 --- a/app/kubernetes/views/deploy/deploy.html +++ b/app/kubernetes/views/deploy/deploy.html @@ -151,6 +151,10 @@ ng-click="ctrl.deploy()" button-spinner="ctrl.state.actionInProgress" data-cy="k8sAppDeploy-deployButton" + analytics-on + analytics-category="kubernetes" + analytics-event="kubernetes-application-advanced-deployment" + analytics-properties="ctrl.buildAnalyticsProperties()" > Deploy Deployment in progress... diff --git a/app/kubernetes/views/deploy/deployController.js b/app/kubernetes/views/deploy/deployController.js index fff228bbe..4614febc1 100644 --- a/app/kubernetes/views/deploy/deployController.js +++ b/app/kubernetes/views/deploy/deployController.js @@ -7,10 +7,11 @@ import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, Kubernetes import { buildOption } from '@/portainer/components/box-selector'; class KubernetesDeployController { /* @ngInject */ - constructor($async, $state, $window, CustomTemplateService, ModalService, Notifications, EndpointProvider, KubernetesResourcePoolService, StackService) { + constructor($async, $state, $window, Authentication, CustomTemplateService, ModalService, Notifications, EndpointProvider, KubernetesResourcePoolService, StackService) { this.$async = $async; this.$state = $state; this.$window = $window; + this.Authentication = Authentication; this.CustomTemplateService = CustomTemplateService; this.ModalService = ModalService; this.Notifications = Notifications; @@ -52,6 +53,47 @@ class KubernetesDeployController { this.onChangeFormValues = this.onChangeFormValues.bind(this); this.onRepoUrlChange = this.onRepoUrlChange.bind(this); this.onRepoRefChange = this.onRepoRefChange.bind(this); + this.buildAnalyticsProperties = this.buildAnalyticsProperties.bind(this); + } + + buildAnalyticsProperties() { + const metadata = { + type: buildLabel(this.state.BuildMethod), + format: formatLabel(this.state.DeployType), + role: roleLabel(this.Authentication.isAdmin()), + }; + + if (this.state.BuildMethod === KubernetesDeployBuildMethods.GIT) { + metadata.auth = this.formValues.RepositoryAuthentication; + } + + return { metadata }; + + function roleLabel(isAdmin) { + if (isAdmin) { + return 'admin'; + } + + return 'standard'; + } + + function buildLabel(buildMethod) { + switch (buildMethod) { + case KubernetesDeployBuildMethods.GIT: + return 'git'; + case KubernetesDeployBuildMethods.WEB_EDITOR: + return 'web-editor'; + } + } + + function formatLabel(format) { + switch (format) { + case KubernetesDeployManifestTypes.COMPOSE: + return 'compose'; + case KubernetesDeployManifestTypes.KUBERNETES: + return 'manifest'; + } + } } disableDeploy() { @@ -59,11 +101,13 @@ class KubernetesDeployController { this.state.BuildMethod === KubernetesDeployBuildMethods.GIT && (!this.formValues.RepositoryURL || !this.formValues.FilePathInRepository || - (this.formValues.RepositoryAuthentication && (!this.formValues.RepositoryUsername || !this.formValues.RepositoryPassword))) && _.isEmpty(this.formValues.Namespace); - const isWebEditorInvalid = this.state.BuildMethod === KubernetesDeployBuildMethods.WEB_EDITOR && _.isEmpty(this.formValues.EditorContent) && _.isEmpty(this.formValues.Namespace); + (this.formValues.RepositoryAuthentication && (!this.formValues.RepositoryUsername || !this.formValues.RepositoryPassword))) && + _.isEmpty(this.formValues.Namespace); + const isWebEditorInvalid = + this.state.BuildMethod === KubernetesDeployBuildMethods.WEB_EDITOR && _.isEmpty(this.formValues.EditorContent) && _.isEmpty(this.formValues.Namespace); const isURLFormInvalid = this.state.BuildMethod == KubernetesDeployBuildMethods.WEB_EDITOR.URL && _.isEmpty(this.formValues.ManifestURL); - return isGitFormInvalid || isWebEditorInvalid || isURLFormInvalid || this.state.actionInProgress; + return isGitFormInvalid || isWebEditorInvalid || isURLFormInvalid || this.state.actionInProgress; } onChangeFormValues(values) { @@ -127,7 +171,7 @@ class KubernetesDeployController { case KubernetesDeployBuildMethods.CUSTOM_TEMPLATE: method = KubernetesDeployRequestMethods.STRING; composeFormat = false; - break; + break; case this.BuildMethods.URL: method = KubernetesDeployRequestMethods.URL; break; diff --git a/app/portainer/components/custom-template-selector/custom-template-selector.controller.js b/app/portainer/components/custom-template-selector/custom-template-selector.controller.js index 04f09be50..c7b82b592 100644 --- a/app/portainer/components/custom-template-selector/custom-template-selector.controller.js +++ b/app/portainer/components/custom-template-selector/custom-template-selector.controller.js @@ -8,8 +8,8 @@ class CustomTemplateSelectorController { } async handleChangeTemplate(templateId) { - this.selectedTemplate = this.templates.find((t) => t.id === templateId); - this.onChange(templateId); + this.selectedTemplate = this.templates.find((t) => t.Id === templateId); + this.onChange(templateId, this.selectedTemplate); } $onChanges({ value }) { diff --git a/app/portainer/components/forms/registry-form-azure/registry-form-azure.html b/app/portainer/components/forms/registry-form-azure/registry-form-azure.html index 705d50921..70dad0ce8 100644 --- a/app/portainer/components/forms/registry-form-azure/registry-form-azure.html +++ b/app/portainer/components/forms/registry-form-azure/registry-form-azure.html @@ -71,7 +71,16 @@ - + {{ $ctrl.formActionLabel }} In progress... diff --git a/app/portainer/components/forms/registry-form-custom/registry-form-custom.html b/app/portainer/components/forms/registry-form-custom/registry-form-custom.html index 7081aff7c..ee77175cc 100644 --- a/app/portainer/components/forms/registry-form-custom/registry-form-custom.html +++ b/app/portainer/components/forms/registry-form-custom/registry-form-custom.html @@ -93,7 +93,16 @@ - + {{ $ctrl.formActionLabel }} In progress... diff --git a/app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.html b/app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.html index ddf26592e..bd9fe4281 100644 --- a/app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.html +++ b/app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.html @@ -54,7 +54,16 @@ - + {{ $ctrl.formActionLabel }} In progress... diff --git a/app/portainer/components/forms/registry-form-gitlab/registry-form-gitlab.html b/app/portainer/components/forms/registry-form-gitlab/registry-form-gitlab.html index 2b64020d4..de276ffb4 100644 --- a/app/portainer/components/forms/registry-form-gitlab/registry-form-gitlab.html +++ b/app/portainer/components/forms/registry-form-gitlab/registry-form-gitlab.html @@ -133,6 +133,10 @@ class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !$ctrl.state.gitlab.selectedItemCount" button-spinner="$ctrl.actionInProgress" + analytics-on + analytics-category="portainer" + analytics-event="portainer-registry-creation" + analytics-properties="{ metadata: { type: 'gitlab' } }" > Create registries In progress... diff --git a/app/portainer/components/forms/registry-form-proget/registry-form-proget.html b/app/portainer/components/forms/registry-form-proget/registry-form-proget.html index f548f1c08..a8c25ac0d 100644 --- a/app/portainer/components/forms/registry-form-proget/registry-form-proget.html +++ b/app/portainer/components/forms/registry-form-proget/registry-form-proget.html @@ -100,7 +100,16 @@ - + {{ $ctrl.formActionLabel }} In progress... diff --git a/app/portainer/components/forms/registry-form-quay/registry-form-quay.html b/app/portainer/components/forms/registry-form-quay/registry-form-quay.html index ff6883e09..28a7c1bc8 100644 --- a/app/portainer/components/forms/registry-form-quay/registry-form-quay.html +++ b/app/portainer/components/forms/registry-form-quay/registry-form-quay.html @@ -67,7 +67,16 @@ - + {{ $ctrl.formActionLabel }} In progress... 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 1d21ae75c..6641f25f0 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 @@ -25,7 +25,7 @@ class StackRedeployGitFormController { RepositoryUsername: '', RepositoryPassword: '', Env: [], - // auto upadte + // auto update AutoUpdate: { RepositoryAutomaticUpdates: false, RepositoryMechanism: 'Interval', @@ -38,6 +38,26 @@ class StackRedeployGitFormController { this.onChangeRef = this.onChangeRef.bind(this); this.onChangeAutoUpdate = this.onChangeAutoUpdate.bind(this); this.onChangeEnvVar = this.onChangeEnvVar.bind(this); + this.handleEnvVarChange = this.handleEnvVarChange.bind(this); + } + + buildAnalyticsProperties() { + const metadata = {}; + + if (this.formValues.RepositoryAutomaticUpdates) { + metadata.automaticUpdates = autoSyncLabel(this.formValues.RepositoryMechanism); + } + return { metadata }; + + function autoSyncLabel(type) { + switch (type) { + case 'Interval': + return 'polling'; + case 'Webhook': + return 'webhook'; + } + return 'off'; + } } onChange(values) { @@ -100,10 +120,17 @@ class StackRedeployGitFormController { return this.$async(async () => { try { this.state.inProgress = true; - await this.StackService.updateGitStackSettings(this.stack.Id, this.stack.EndpointId, this.FormHelper.removeInvalidEnvVars(this.formValues.Env), this.formValues); + const stack = await this.StackService.updateGitStackSettings( + this.stack.Id, + this.stack.EndpointId, + this.FormHelper.removeInvalidEnvVars(this.formValues.Env), + this.formValues + ); this.savedFormValues = angular.copy(this.formValues); this.state.hasUnsavedChanges = false; this.Notifications.success('Save stack settings successfully'); + + this.stack = stack; } catch (err) { this.Notifications.error('Failure', err, 'Unable to save stack settings'); } finally { @@ -116,6 +143,16 @@ class StackRedeployGitFormController { return this.state.inProgress || this.state.redeployInProgress; } + handleEnvVarChange(value) { + this.formValues.Env = value; + } + + isAutoUpdateChanged() { + const wasEnabled = !!(this.stack.AutoUpdate && (this.stack.AutoUpdate.Interval || this.stack.AutoUpdate.Webhook)); + const isEnabled = this.formValues.AutoUpdate.RepositoryAutomaticUpdates; + return isEnabled !== wasEnabled; + } + $onInit() { this.formValues.RefName = this.model.ReferenceName; this.formValues.Env = this.stack.Env; 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 07b85c51f..5d9025438 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 @@ -50,8 +50,9 @@ ng-disabled="$ctrl.isSubmitButtonDisabled() || $ctrl.state.hasUnsavedChanges || !$ctrl.redeployGitForm.$valid" style="margin-top: 7px; margin-left: 0;" button-spinner="$ctrl.state.redeployInProgress" - style="margin-top: 7px; margin-left: 0;" - button-spinner="$ctrl.state.inProgress" + analytics-on + analytics-event="docker-stack-pull-redeploy" + analytics-category="docker" > Pull and redeploy In progress... @@ -63,6 +64,10 @@ ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.state.hasUnsavedChanges || !$ctrl.redeployGitForm.$valid" style="margin-top: 7px; margin-left: 0;" button-spinner="$ctrl.state.inProgress" + analytics-on + analytics-event="docker-stack-update-git-settings" + analytics-category="docker" + analytics-properties="$ctrl.buildAnalyticsProperties()" > Save settings In progress... diff --git a/app/portainer/settings/general/ssl-certificate/ssl-certificate.controller.js b/app/portainer/settings/general/ssl-certificate/ssl-certificate.controller.js index 52fd30ce6..681d697ea 100644 --- a/app/portainer/settings/general/ssl-certificate/ssl-certificate.controller.js +++ b/app/portainer/settings/general/ssl-certificate/ssl-certificate.controller.js @@ -51,6 +51,10 @@ class SslCertificateController { }); } + wasHTTPsChanged() { + return this.originalValues.forceHTTPS !== this.formValues.forceHTTPS; + } + async $onInit() { return this.$async(async () => { try { diff --git a/app/portainer/settings/general/ssl-certificate/ssl-certificate.html b/app/portainer/settings/general/ssl-certificate/ssl-certificate.html index 83fa54aa5..84c52b69d 100644 --- a/app/portainer/settings/general/ssl-certificate/ssl-certificate.html +++ b/app/portainer/settings/general/ssl-certificate/ssl-certificate.html @@ -80,6 +80,11 @@ ng-disabled="$ctrl.state.actionInProgress || !$ctrl.isFormChanged()" ng-click="$ctrl.save()" button-spinner="$ctrl.state.actionInProgress" + analytics-on + analytics-if="$ctrl.wasHTTPsChanged()" + analytics-category="portainer" + analytics-event="portainer-settings-edit" + analytics-properties="{ metadata: { forceHTTPS: $ctrl.formValues.forceHTTPS } }" > Apply Changes Saving in progress... diff --git a/app/portainer/views/endpoints/create/createEndpointController.js b/app/portainer/views/endpoints/create/createEndpointController.js index 4a2d80e30..f8d983456 100644 --- a/app/portainer/views/endpoints/create/createEndpointController.js +++ b/app/portainer/views/endpoints/create/createEndpointController.js @@ -5,6 +5,7 @@ angular .module('portainer.app') .controller('CreateEndpointController', function CreateEndpointController( $async, + $analytics, $q, $scope, $state, @@ -167,16 +168,32 @@ angular }); }; - $scope.addAgentEndpoint = function () { - var name = $scope.formValues.Name; - // var URL = $filter('stripprotocol')($scope.formValues.URL); - var URL = $scope.formValues.URL; - var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL; - var groupId = $scope.formValues.GroupId; - var tagIds = $scope.formValues.TagIds; + $scope.addAgentEndpoint = addAgentEndpoint; + async function addAgentEndpoint() { + return $async(async () => { + const name = $scope.formValues.Name; + const URL = $scope.formValues.URL; + const publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL; + const groupId = $scope.formValues.GroupId; + const tagIds = $scope.formValues.TagIds; - addEndpoint(name, PortainerEndpointCreationTypes.AgentEnvironment, URL, publicURL, groupId, tagIds, true, true, true, null, null, null); - }; + const endpoint = await addEndpoint(name, PortainerEndpointCreationTypes.AgentEnvironment, URL, publicURL, groupId, tagIds, true, true, true, null, null, null); + $analytics.eventTrack('portainer-endpoint-creation', { category: 'portainer', metadata: { type: 'agent', platform: platformLabel(endpoint.Type) } }); + }); + + function platformLabel(type) { + switch (type) { + case PortainerEndpointTypes.DockerEnvironment: + case PortainerEndpointTypes.AgentOnDockerEnvironment: + case PortainerEndpointTypes.EdgeAgentOnDockerEnvironment: + return 'docker'; + case PortainerEndpointTypes.KubernetesLocalEnvironment: + case PortainerEndpointTypes.AgentOnKubernetesEnvironment: + case PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment: + return 'kubernetes'; + } + } + } $scope.addEdgeAgentEndpoint = function () { var name = $scope.formValues.Name; @@ -213,24 +230,26 @@ angular }); } - function addEndpoint(name, creationType, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, CheckinInterval) { - $scope.state.actionInProgress = true; - EndpointService.createRemoteEndpoint( - name, - creationType, - URL, - PublicURL, - groupId, - tagIds, - TLS, - TLSSkipVerify, - TLSSkipClientVerify, - TLSCAFile, - TLSCertFile, - TLSKeyFile, - CheckinInterval - ) - .then(function success(endpoint) { + async function addEndpoint(name, creationType, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, CheckinInterval) { + return $async(async () => { + $scope.state.actionInProgress = true; + try { + const endpoint = await EndpointService.createRemoteEndpoint( + name, + creationType, + URL, + PublicURL, + groupId, + tagIds, + TLS, + TLSSkipVerify, + TLSSkipClientVerify, + TLSCAFile, + TLSCertFile, + TLSKeyFile, + CheckinInterval + ); + Notifications.success('Endpoint created', name); switch (endpoint.Type) { case PortainerEndpointTypes.EdgeAgentOnDockerEnvironment: @@ -244,13 +263,14 @@ angular $state.go('portainer.endpoints', {}, { reload: true }); break; } - }) - .catch(function error(err) { + + return endpoint; + } catch (err) { Notifications.error('Failure', err, 'Unable to create endpoint'); - }) - .finally(function final() { + } finally { $scope.state.actionInProgress = false; - }); + } + }); } function initView() { diff --git a/app/portainer/views/endpoints/create/createendpoint.html b/app/portainer/views/endpoints/create/createendpoint.html index 717a3cfca..3827ff671 100644 --- a/app/portainer/views/endpoints/create/createendpoint.html +++ b/app/portainer/views/endpoints/create/createendpoint.html @@ -482,6 +482,10 @@ ng-click="addDockerEndpoint()" button-spinner="state.actionInProgress" data-cy="endpointCreate-createDockerEndpoint" + analytics-on + analytics-category="portainer" + analytics-event="portainer-endpoint-creation" + analytics-properties="{ metadata: { type: 'docker-api' } }" > Add endpoint Creating endpoint... @@ -506,6 +510,10 @@ ng-click="addEdgeAgentEndpoint()" button-spinner="state.actionInProgress" data-cy="endpointCreate-createEdgeAgentEndpoint" + analytics-on + analytics-category="portainer" + analytics-event="portainer-endpoint-creation" + analytics-properties="{ metadata: { type: 'edge-agent' } }" > Add endpoint Creating endpoint... @@ -517,6 +525,10 @@ ng-disabled="state.actionInProgress || !endpointCreationForm.$valid" ng-click="addKubernetesEndpoint()" button-spinner="state.actionInProgress" + analytics-on + analytics-category="portainer" + analytics-event="portainer-endpoint-creation" + analytics-properties="{ metadata: { type: 'kubernetes-api' } }" > Add endpoint Creating endpoint... @@ -529,6 +541,10 @@ ng-click="addAzureEndpoint()" button-spinner="state.actionInProgress" data-cy="endpointCreate-createAzureEndpoint" + analytics-on + analytics-category="portainer" + analytics-event="portainer-endpoint-creation" + analytics-properties="{ metadata: { type: 'azure-api' } }" > Add endpoint Creating endpoint... diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html index d28fcd065..1078a01e2 100644 --- a/app/portainer/views/endpoints/edit/endpoint.html +++ b/app/portainer/views/endpoints/edit/endpoint.html @@ -23,7 +23,16 @@ Edge identifier: {{ endpoint.EdgeID }} - + De-associate diff --git a/app/portainer/views/stacks/create/createStackController.js b/app/portainer/views/stacks/create/createStackController.js index 30e14cd59..d01646ef8 100644 --- a/app/portainer/views/stacks/create/createStackController.js +++ b/app/portainer/views/stacks/create/createStackController.js @@ -26,6 +26,7 @@ angular clipboard ) { $scope.onChangeTemplateId = onChangeTemplateId; + $scope.buildAnalyticsProperties = buildAnalyticsProperties; $scope.formValues = { Name: '', @@ -54,6 +55,8 @@ angular editorYamlValidationError: '', uploadYamlValidationError: '', isEditorDirty: false, + selectedTemplate: null, + selectedTemplateId: null, }; $window.onbeforeunload = () => { @@ -76,6 +79,47 @@ angular $scope.formValues.AdditionalFiles.splice(index, 1); }; + function buildAnalyticsProperties() { + const metadata = { type: methodLabel($scope.state.Method) }; + + if ($scope.state.Method === 'repository') { + metadata.automaticUpdates = 'off'; + if ($scope.formValues.RepositoryAutomaticUpdates) { + metadata.automaticUpdates = autoSyncLabel($scope.formValues.RepositoryMechanism); + } + metadata.auth = $scope.formValues.RepositoryAuthentication; + } + + if ($scope.state.Method === 'template') { + metadata.templateName = $scope.state.selectedTemplate.Title; + } + + return { metadata }; + + function methodLabel(method) { + switch (method) { + case 'editor': + return 'web-editor'; + case 'repository': + return 'git'; + case 'upload': + return 'file-upload'; + case 'template': + return 'custom-template'; + } + } + + function autoSyncLabel(type) { + switch (type) { + case 'Interval': + return 'polling'; + case 'Webhook': + return 'webhook'; + } + return 'off'; + } + } + function validateForm(accessControlData, isAdmin) { $scope.state.formValidationError = ''; var error = ''; @@ -238,10 +282,11 @@ angular } }; - function onChangeTemplateId(templateId) { + function onChangeTemplateId(templateId, template) { return $async(async () => { try { - $scope.state.templateId = templateId; + $scope.state.selectedTemplateId = templateId; + $scope.state.selectedTemplate = template; const fileContent = await CustomTemplateService.customTemplateFile(templateId); $scope.onChangeFileContent(fileContent); diff --git a/app/portainer/views/stacks/create/createstack.html b/app/portainer/views/stacks/create/createstack.html index db9b28dda..9d32ca5df 100644 --- a/app/portainer/views/stacks/create/createstack.html +++ b/app/portainer/views/stacks/create/createstack.html @@ -120,11 +120,11 @@ new-template-path="docker.templates.custom.new" stack-type="state.StackType" on-change="(onChangeTemplateId)" - value="state.templateId" + value="state.selectedTemplateId" > Deploy the stack Deployment in progress...
{{ endpoint.EdgeID }}
- + De-associate