mirror of https://github.com/portainer/portainer
feat(analytics): track existing features (#5448) [EE-1076]
parent
b8e6c5ea91
commit
4ffee27a4b
|
@ -163,5 +163,19 @@
|
||||||
"// @failure 500 \"Server error\"",
|
"// @failure 500 \"Server error\"",
|
||||||
"// @router /{id} [get]"
|
"// @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 } }\""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
const basePath = 'http://portainer-ce.app';
|
const basePath = 'http://portainer-ce.app';
|
||||||
|
|
||||||
|
@ -131,7 +132,8 @@ function config($analyticsProvider, $windowProvider) {
|
||||||
|
|
||||||
let metadataString = '';
|
let metadataString = '';
|
||||||
if (metadata) {
|
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([
|
push([
|
||||||
|
|
|
@ -199,6 +199,10 @@
|
||||||
ng-click="$ctrl.createStack()"
|
ng-click="$ctrl.createStack()"
|
||||||
button-spinner="$ctrl.state.actionInProgress"
|
button-spinner="$ctrl.state.actionInProgress"
|
||||||
data-cy="edgeStackCreate-createStackButton"
|
data-cy="edgeStackCreate-createStackButton"
|
||||||
|
analytics-on
|
||||||
|
analytics-event="edge-stack-creation"
|
||||||
|
analytics-category="edge"
|
||||||
|
analytics-properties="$ctrl.buildAnalyticsProperties()"
|
||||||
>
|
>
|
||||||
<span ng-hide="$ctrl.state.actionInProgress">Deploy the stack</span>
|
<span ng-hide="$ctrl.state.actionInProgress">Deploy the stack</span>
|
||||||
<span ng-show="$ctrl.state.actionInProgress">Deployment in progress...</span>
|
<span ng-show="$ctrl.state.actionInProgress">Deployment in progress...</span>
|
||||||
|
|
|
@ -43,6 +43,30 @@ export class CreateEdgeStackViewController {
|
||||||
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
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() {
|
async uiCanExit() {
|
||||||
if (this.state.Method === 'editor' && this.formValues.StackFileContent && this.state.isEditorDirty) {
|
if (this.state.Method === 'editor' && this.formValues.StackFileContent && this.state.isEditorDirty) {
|
||||||
return this.ModalService.confirmWebEditorDiscard();
|
return this.ModalService.confirmWebEditorDiscard();
|
||||||
|
|
|
@ -1,4 +1,13 @@
|
||||||
<button type="button" class="btn btn-xs btn-primary" ng-click="$ctrl.connectConsole()" ng-disabled="$ctrl.state.shell.connected" data-cy="k8sSidebar-shellButton">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-xs btn-primary"
|
||||||
|
ng-click="$ctrl.connectConsole()"
|
||||||
|
ng-disabled="$ctrl.state.shell.connected"
|
||||||
|
data-cy="k8sSidebar-shellButton"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="kubernetes"
|
||||||
|
analytics-event="kubernetes-kubectl-shell"
|
||||||
|
>
|
||||||
<i class="fa fa-terminal space-right"></i> kubectl shell
|
<i class="fa fa-terminal space-right"></i> kubectl shell
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
|
@ -299,6 +299,11 @@
|
||||||
ng-click="ctrl.configure()"
|
ng-click="ctrl.configure()"
|
||||||
ng-disabled="ctrl.state.actionInProgress || !kubernetesClusterSetupForm.$valid || !ctrl.hasValidStorageConfiguration()"
|
ng-disabled="ctrl.state.actionInProgress || !kubernetesClusterSetupForm.$valid || !ctrl.hasValidStorageConfiguration()"
|
||||||
button-spinner="ctrl.state.actionInProgress"
|
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 } }"
|
||||||
>
|
>
|
||||||
<span ng-hide="ctrl.state.actionInProgress">Save configuration</span>
|
<span ng-hide="ctrl.state.actionInProgress">Save configuration</span>
|
||||||
<span ng-show="ctrl.state.actionInProgress">Saving configuration...</span>
|
<span ng-show="ctrl.state.actionInProgress">Saving configuration...</span>
|
||||||
|
|
|
@ -237,6 +237,10 @@ class KubernetesConfigureController {
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
restrictDefaultToggledOn() {
|
||||||
|
return this.formValues.RestrictDefaultNamespace && !this.oldFormValues.RestrictDefaultNamespace;
|
||||||
|
}
|
||||||
|
|
||||||
/* #region ON INIT */
|
/* #region ON INIT */
|
||||||
async onInit() {
|
async onInit() {
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -287,6 +291,8 @@ class KubernetesConfigureController {
|
||||||
ic.NeedsDeletion = false;
|
ic.NeedsDeletion = false;
|
||||||
return ic;
|
return ic;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.oldFormValues = Object.assign({}, this.formValues);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve endpoint configuration');
|
this.Notifications.error('Failure', err, 'Unable to retrieve endpoint configuration');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -151,6 +151,10 @@
|
||||||
ng-click="ctrl.deploy()"
|
ng-click="ctrl.deploy()"
|
||||||
button-spinner="ctrl.state.actionInProgress"
|
button-spinner="ctrl.state.actionInProgress"
|
||||||
data-cy="k8sAppDeploy-deployButton"
|
data-cy="k8sAppDeploy-deployButton"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="kubernetes"
|
||||||
|
analytics-event="kubernetes-application-advanced-deployment"
|
||||||
|
analytics-properties="ctrl.buildAnalyticsProperties()"
|
||||||
>
|
>
|
||||||
<span ng-hide="ctrl.state.actionInProgress">Deploy</span>
|
<span ng-hide="ctrl.state.actionInProgress">Deploy</span>
|
||||||
<span ng-show="ctrl.state.actionInProgress">Deployment in progress...</span>
|
<span ng-show="ctrl.state.actionInProgress">Deployment in progress...</span>
|
||||||
|
|
|
@ -7,10 +7,11 @@ import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, Kubernetes
|
||||||
import { buildOption } from '@/portainer/components/box-selector';
|
import { buildOption } from '@/portainer/components/box-selector';
|
||||||
class KubernetesDeployController {
|
class KubernetesDeployController {
|
||||||
/* @ngInject */
|
/* @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.$async = $async;
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
this.$window = $window;
|
this.$window = $window;
|
||||||
|
this.Authentication = Authentication;
|
||||||
this.CustomTemplateService = CustomTemplateService;
|
this.CustomTemplateService = CustomTemplateService;
|
||||||
this.ModalService = ModalService;
|
this.ModalService = ModalService;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
|
@ -52,6 +53,47 @@ class KubernetesDeployController {
|
||||||
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
||||||
this.onRepoUrlChange = this.onRepoUrlChange.bind(this);
|
this.onRepoUrlChange = this.onRepoUrlChange.bind(this);
|
||||||
this.onRepoRefChange = this.onRepoRefChange.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() {
|
disableDeploy() {
|
||||||
|
@ -59,8 +101,10 @@ class KubernetesDeployController {
|
||||||
this.state.BuildMethod === KubernetesDeployBuildMethods.GIT &&
|
this.state.BuildMethod === KubernetesDeployBuildMethods.GIT &&
|
||||||
(!this.formValues.RepositoryURL ||
|
(!this.formValues.RepositoryURL ||
|
||||||
!this.formValues.FilePathInRepository ||
|
!this.formValues.FilePathInRepository ||
|
||||||
(this.formValues.RepositoryAuthentication && (!this.formValues.RepositoryUsername || !this.formValues.RepositoryPassword))) && _.isEmpty(this.formValues.Namespace);
|
(this.formValues.RepositoryAuthentication && (!this.formValues.RepositoryUsername || !this.formValues.RepositoryPassword))) &&
|
||||||
const isWebEditorInvalid = this.state.BuildMethod === KubernetesDeployBuildMethods.WEB_EDITOR && _.isEmpty(this.formValues.EditorContent) && _.isEmpty(this.formValues.Namespace);
|
_.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);
|
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;
|
||||||
|
|
|
@ -8,8 +8,8 @@ class CustomTemplateSelectorController {
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleChangeTemplate(templateId) {
|
async handleChangeTemplate(templateId) {
|
||||||
this.selectedTemplate = this.templates.find((t) => t.id === templateId);
|
this.selectedTemplate = this.templates.find((t) => t.Id === templateId);
|
||||||
this.onChange(templateId);
|
this.onChange(templateId, this.selectedTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
$onChanges({ value }) {
|
$onChanges({ value }) {
|
||||||
|
|
|
@ -71,7 +71,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormAzure.$valid" button-spinner="$ctrl.actionInProgress">
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
ng-disabled="$ctrl.actionInProgress || !registryFormAzure.$valid"
|
||||||
|
button-spinner="$ctrl.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-registry-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'azure' } }"
|
||||||
|
>
|
||||||
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
||||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -93,7 +93,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormCustom.$valid" button-spinner="$ctrl.actionInProgress">
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
ng-disabled="$ctrl.actionInProgress || !registryFormCustom.$valid"
|
||||||
|
button-spinner="$ctrl.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-registry-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'custom' }}"
|
||||||
|
>
|
||||||
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
||||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -54,7 +54,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormDockerhub.$valid" button-spinner="$ctrl.actionInProgress">
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
ng-disabled="$ctrl.actionInProgress || !registryFormDockerhub.$valid"
|
||||||
|
button-spinner="$ctrl.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-registry-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'dockerhub' } }"
|
||||||
|
>
|
||||||
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
||||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -133,6 +133,10 @@
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
ng-disabled="$ctrl.actionInProgress || !$ctrl.state.gitlab.selectedItemCount"
|
ng-disabled="$ctrl.actionInProgress || !$ctrl.state.gitlab.selectedItemCount"
|
||||||
button-spinner="$ctrl.actionInProgress"
|
button-spinner="$ctrl.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-registry-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'gitlab' } }"
|
||||||
>
|
>
|
||||||
<span ng-hide="$ctrl.actionInProgress">Create registries</span>
|
<span ng-hide="$ctrl.actionInProgress">Create registries</span>
|
||||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||||
|
|
|
@ -100,7 +100,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormProGet.$valid" button-spinner="$ctrl.actionInProgress">
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
ng-disabled="$ctrl.actionInProgress || !registryFormProGet.$valid"
|
||||||
|
button-spinner="$ctrl.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-registry-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'proget' } }"
|
||||||
|
>
|
||||||
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
||||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -67,7 +67,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormQuay.$valid" button-spinner="$ctrl.actionInProgress">
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
ng-disabled="$ctrl.actionInProgress || !registryFormQuay.$valid"
|
||||||
|
button-spinner="$ctrl.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-registry-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'quay' } }"
|
||||||
|
>
|
||||||
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
||||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -25,7 +25,7 @@ class StackRedeployGitFormController {
|
||||||
RepositoryUsername: '',
|
RepositoryUsername: '',
|
||||||
RepositoryPassword: '',
|
RepositoryPassword: '',
|
||||||
Env: [],
|
Env: [],
|
||||||
// auto upadte
|
// auto update
|
||||||
AutoUpdate: {
|
AutoUpdate: {
|
||||||
RepositoryAutomaticUpdates: false,
|
RepositoryAutomaticUpdates: false,
|
||||||
RepositoryMechanism: 'Interval',
|
RepositoryMechanism: 'Interval',
|
||||||
|
@ -38,6 +38,26 @@ class StackRedeployGitFormController {
|
||||||
this.onChangeRef = this.onChangeRef.bind(this);
|
this.onChangeRef = this.onChangeRef.bind(this);
|
||||||
this.onChangeAutoUpdate = this.onChangeAutoUpdate.bind(this);
|
this.onChangeAutoUpdate = this.onChangeAutoUpdate.bind(this);
|
||||||
this.onChangeEnvVar = this.onChangeEnvVar.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) {
|
onChange(values) {
|
||||||
|
@ -100,10 +120,17 @@ class StackRedeployGitFormController {
|
||||||
return this.$async(async () => {
|
return this.$async(async () => {
|
||||||
try {
|
try {
|
||||||
this.state.inProgress = true;
|
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.savedFormValues = angular.copy(this.formValues);
|
||||||
this.state.hasUnsavedChanges = false;
|
this.state.hasUnsavedChanges = false;
|
||||||
this.Notifications.success('Save stack settings successfully');
|
this.Notifications.success('Save stack settings successfully');
|
||||||
|
|
||||||
|
this.stack = stack;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to save stack settings');
|
this.Notifications.error('Failure', err, 'Unable to save stack settings');
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -116,6 +143,16 @@ class StackRedeployGitFormController {
|
||||||
return this.state.inProgress || this.state.redeployInProgress;
|
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() {
|
$onInit() {
|
||||||
this.formValues.RefName = this.model.ReferenceName;
|
this.formValues.RefName = this.model.ReferenceName;
|
||||||
this.formValues.Env = this.stack.Env;
|
this.formValues.Env = this.stack.Env;
|
||||||
|
|
|
@ -50,8 +50,9 @@
|
||||||
ng-disabled="$ctrl.isSubmitButtonDisabled() || $ctrl.state.hasUnsavedChanges || !$ctrl.redeployGitForm.$valid"
|
ng-disabled="$ctrl.isSubmitButtonDisabled() || $ctrl.state.hasUnsavedChanges || !$ctrl.redeployGitForm.$valid"
|
||||||
style="margin-top: 7px; margin-left: 0;"
|
style="margin-top: 7px; margin-left: 0;"
|
||||||
button-spinner="$ctrl.state.redeployInProgress"
|
button-spinner="$ctrl.state.redeployInProgress"
|
||||||
style="margin-top: 7px; margin-left: 0;"
|
analytics-on
|
||||||
button-spinner="$ctrl.state.inProgress"
|
analytics-event="docker-stack-pull-redeploy"
|
||||||
|
analytics-category="docker"
|
||||||
>
|
>
|
||||||
<span ng-hide="$ctrl.state.redeployInProgress"> <i class="fa fa-sync space-right" aria-hidden="true"></i> Pull and redeploy </span>
|
<span ng-hide="$ctrl.state.redeployInProgress"> <i class="fa fa-sync space-right" aria-hidden="true"></i> Pull and redeploy </span>
|
||||||
<span ng-show="$ctrl.state.redeployInProgress">In progress...</span>
|
<span ng-show="$ctrl.state.redeployInProgress">In progress...</span>
|
||||||
|
@ -63,6 +64,10 @@
|
||||||
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.state.hasUnsavedChanges || !$ctrl.redeployGitForm.$valid"
|
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.state.hasUnsavedChanges || !$ctrl.redeployGitForm.$valid"
|
||||||
style="margin-top: 7px; margin-left: 0;"
|
style="margin-top: 7px; margin-left: 0;"
|
||||||
button-spinner="$ctrl.state.inProgress"
|
button-spinner="$ctrl.state.inProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-event="docker-stack-update-git-settings"
|
||||||
|
analytics-category="docker"
|
||||||
|
analytics-properties="$ctrl.buildAnalyticsProperties()"
|
||||||
>
|
>
|
||||||
<span ng-hide="$ctrl.state.inProgress"> Save settings </span>
|
<span ng-hide="$ctrl.state.inProgress"> Save settings </span>
|
||||||
<span ng-show="$ctrl.state.inProgress">In progress...</span>
|
<span ng-show="$ctrl.state.inProgress">In progress...</span>
|
||||||
|
|
|
@ -51,6 +51,10 @@ class SslCertificateController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wasHTTPsChanged() {
|
||||||
|
return this.originalValues.forceHTTPS !== this.formValues.forceHTTPS;
|
||||||
|
}
|
||||||
|
|
||||||
async $onInit() {
|
async $onInit() {
|
||||||
return this.$async(async () => {
|
return this.$async(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -80,6 +80,11 @@
|
||||||
ng-disabled="$ctrl.state.actionInProgress || !$ctrl.isFormChanged()"
|
ng-disabled="$ctrl.state.actionInProgress || !$ctrl.isFormChanged()"
|
||||||
ng-click="$ctrl.save()"
|
ng-click="$ctrl.save()"
|
||||||
button-spinner="$ctrl.state.actionInProgress"
|
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 } }"
|
||||||
>
|
>
|
||||||
<span ng-hide="$ctrl.state.actionInProgress || $ctrl.state.reloadingPage">Apply Changes</span>
|
<span ng-hide="$ctrl.state.actionInProgress || $ctrl.state.reloadingPage">Apply Changes</span>
|
||||||
<span ng-show="$ctrl.state.actionInProgress">Saving in progress...</span>
|
<span ng-show="$ctrl.state.actionInProgress">Saving in progress...</span>
|
||||||
|
|
|
@ -5,6 +5,7 @@ angular
|
||||||
.module('portainer.app')
|
.module('portainer.app')
|
||||||
.controller('CreateEndpointController', function CreateEndpointController(
|
.controller('CreateEndpointController', function CreateEndpointController(
|
||||||
$async,
|
$async,
|
||||||
|
$analytics,
|
||||||
$q,
|
$q,
|
||||||
$scope,
|
$scope,
|
||||||
$state,
|
$state,
|
||||||
|
@ -167,16 +168,32 @@ angular
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.addAgentEndpoint = function () {
|
$scope.addAgentEndpoint = addAgentEndpoint;
|
||||||
var name = $scope.formValues.Name;
|
async function addAgentEndpoint() {
|
||||||
// var URL = $filter('stripprotocol')($scope.formValues.URL);
|
return $async(async () => {
|
||||||
var URL = $scope.formValues.URL;
|
const name = $scope.formValues.Name;
|
||||||
var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL;
|
const URL = $scope.formValues.URL;
|
||||||
var groupId = $scope.formValues.GroupId;
|
const publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL;
|
||||||
var tagIds = $scope.formValues.TagIds;
|
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 () {
|
$scope.addEdgeAgentEndpoint = function () {
|
||||||
var name = $scope.formValues.Name;
|
var name = $scope.formValues.Name;
|
||||||
|
@ -213,9 +230,11 @@ angular
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addEndpoint(name, creationType, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, CheckinInterval) {
|
async function addEndpoint(name, creationType, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, CheckinInterval) {
|
||||||
|
return $async(async () => {
|
||||||
$scope.state.actionInProgress = true;
|
$scope.state.actionInProgress = true;
|
||||||
EndpointService.createRemoteEndpoint(
|
try {
|
||||||
|
const endpoint = await EndpointService.createRemoteEndpoint(
|
||||||
name,
|
name,
|
||||||
creationType,
|
creationType,
|
||||||
URL,
|
URL,
|
||||||
|
@ -229,8 +248,8 @@ angular
|
||||||
TLSCertFile,
|
TLSCertFile,
|
||||||
TLSKeyFile,
|
TLSKeyFile,
|
||||||
CheckinInterval
|
CheckinInterval
|
||||||
)
|
);
|
||||||
.then(function success(endpoint) {
|
|
||||||
Notifications.success('Endpoint created', name);
|
Notifications.success('Endpoint created', name);
|
||||||
switch (endpoint.Type) {
|
switch (endpoint.Type) {
|
||||||
case PortainerEndpointTypes.EdgeAgentOnDockerEnvironment:
|
case PortainerEndpointTypes.EdgeAgentOnDockerEnvironment:
|
||||||
|
@ -244,12 +263,13 @@ angular
|
||||||
$state.go('portainer.endpoints', {}, { reload: true });
|
$state.go('portainer.endpoints', {}, { reload: true });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
return endpoint;
|
||||||
|
} catch (err) {
|
||||||
Notifications.error('Failure', err, 'Unable to create endpoint');
|
Notifications.error('Failure', err, 'Unable to create endpoint');
|
||||||
})
|
} finally {
|
||||||
.finally(function final() {
|
|
||||||
$scope.state.actionInProgress = false;
|
$scope.state.actionInProgress = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -482,6 +482,10 @@
|
||||||
ng-click="addDockerEndpoint()"
|
ng-click="addDockerEndpoint()"
|
||||||
button-spinner="state.actionInProgress"
|
button-spinner="state.actionInProgress"
|
||||||
data-cy="endpointCreate-createDockerEndpoint"
|
data-cy="endpointCreate-createDockerEndpoint"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-endpoint-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'docker-api' } }"
|
||||||
>
|
>
|
||||||
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
||||||
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
||||||
|
@ -506,6 +510,10 @@
|
||||||
ng-click="addEdgeAgentEndpoint()"
|
ng-click="addEdgeAgentEndpoint()"
|
||||||
button-spinner="state.actionInProgress"
|
button-spinner="state.actionInProgress"
|
||||||
data-cy="endpointCreate-createEdgeAgentEndpoint"
|
data-cy="endpointCreate-createEdgeAgentEndpoint"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-endpoint-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'edge-agent' } }"
|
||||||
>
|
>
|
||||||
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
||||||
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
||||||
|
@ -517,6 +525,10 @@
|
||||||
ng-disabled="state.actionInProgress || !endpointCreationForm.$valid"
|
ng-disabled="state.actionInProgress || !endpointCreationForm.$valid"
|
||||||
ng-click="addKubernetesEndpoint()"
|
ng-click="addKubernetesEndpoint()"
|
||||||
button-spinner="state.actionInProgress"
|
button-spinner="state.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-endpoint-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'kubernetes-api' } }"
|
||||||
>
|
>
|
||||||
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
||||||
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
||||||
|
@ -529,6 +541,10 @@
|
||||||
ng-click="addAzureEndpoint()"
|
ng-click="addAzureEndpoint()"
|
||||||
button-spinner="state.actionInProgress"
|
button-spinner="state.actionInProgress"
|
||||||
data-cy="endpointCreate-createAzureEndpoint"
|
data-cy="endpointCreate-createAzureEndpoint"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-endpoint-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'azure-api' } }"
|
||||||
>
|
>
|
||||||
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
||||||
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
||||||
|
|
|
@ -23,7 +23,16 @@
|
||||||
Edge identifier: <code>{{ endpoint.EdgeID }}</code>
|
Edge identifier: <code>{{ endpoint.EdgeID }}</code>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress" ng-click="onDeassociateEndpoint()" button-spinner="state.actionInProgress">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
ng-disabled="state.actionInProgress"
|
||||||
|
ng-click="onDeassociateEndpoint()"
|
||||||
|
button-spinner="state.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-event="edge-endpoint-deassociate"
|
||||||
|
analytics-category="edge"
|
||||||
|
>
|
||||||
<span ng-hide="state.actionInProgress">De-associate</span>
|
<span ng-hide="state.actionInProgress">De-associate</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -26,6 +26,7 @@ angular
|
||||||
clipboard
|
clipboard
|
||||||
) {
|
) {
|
||||||
$scope.onChangeTemplateId = onChangeTemplateId;
|
$scope.onChangeTemplateId = onChangeTemplateId;
|
||||||
|
$scope.buildAnalyticsProperties = buildAnalyticsProperties;
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
Name: '',
|
Name: '',
|
||||||
|
@ -54,6 +55,8 @@ angular
|
||||||
editorYamlValidationError: '',
|
editorYamlValidationError: '',
|
||||||
uploadYamlValidationError: '',
|
uploadYamlValidationError: '',
|
||||||
isEditorDirty: false,
|
isEditorDirty: false,
|
||||||
|
selectedTemplate: null,
|
||||||
|
selectedTemplateId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
$window.onbeforeunload = () => {
|
$window.onbeforeunload = () => {
|
||||||
|
@ -76,6 +79,47 @@ angular
|
||||||
$scope.formValues.AdditionalFiles.splice(index, 1);
|
$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) {
|
function validateForm(accessControlData, isAdmin) {
|
||||||
$scope.state.formValidationError = '';
|
$scope.state.formValidationError = '';
|
||||||
var error = '';
|
var error = '';
|
||||||
|
@ -238,10 +282,11 @@ angular
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function onChangeTemplateId(templateId) {
|
function onChangeTemplateId(templateId, template) {
|
||||||
return $async(async () => {
|
return $async(async () => {
|
||||||
try {
|
try {
|
||||||
$scope.state.templateId = templateId;
|
$scope.state.selectedTemplateId = templateId;
|
||||||
|
$scope.state.selectedTemplate = template;
|
||||||
|
|
||||||
const fileContent = await CustomTemplateService.customTemplateFile(templateId);
|
const fileContent = await CustomTemplateService.customTemplateFile(templateId);
|
||||||
$scope.onChangeFileContent(fileContent);
|
$scope.onChangeFileContent(fileContent);
|
||||||
|
|
|
@ -120,11 +120,11 @@
|
||||||
new-template-path="docker.templates.custom.new"
|
new-template-path="docker.templates.custom.new"
|
||||||
stack-type="state.StackType"
|
stack-type="state.StackType"
|
||||||
on-change="(onChangeTemplateId)"
|
on-change="(onChangeTemplateId)"
|
||||||
value="state.templateId"
|
value="state.selectedTemplateId"
|
||||||
></custom-template-selector>
|
></custom-template-selector>
|
||||||
|
|
||||||
<web-editor-form
|
<web-editor-form
|
||||||
ng-if="state.Method === 'editor' || (state.Method === 'template' && state.templateId)"
|
ng-if="state.Method === 'editor' || (state.Method === 'template' && state.selectedTemplateId)"
|
||||||
identifier="stack-creation-editor"
|
identifier="stack-creation-editor"
|
||||||
value="formValues.StackFileContent"
|
value="formValues.StackFileContent"
|
||||||
on-change="(onChangeFileContent)"
|
on-change="(onChangeFileContent)"
|
||||||
|
@ -164,6 +164,10 @@
|
||||||
|| !formValues.Name"
|
|| !formValues.Name"
|
||||||
ng-click="deployStack()"
|
ng-click="deployStack()"
|
||||||
button-spinner="state.actionInProgress"
|
button-spinner="state.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="docker"
|
||||||
|
analytics-event="docker-stack-create"
|
||||||
|
analytics-properties="buildAnalyticsProperties()"
|
||||||
>
|
>
|
||||||
<span ng-hide="state.actionInProgress">Deploy the stack</span>
|
<span ng-hide="state.actionInProgress">Deploy the stack</span>
|
||||||
<span ng-show="state.actionInProgress">Deployment in progress...</span>
|
<span ng-show="state.actionInProgress">Deployment in progress...</span>
|
||||||
|
|
Loading…
Reference in New Issue