1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-08 15:25:22 +02:00

refactor(gitops): migrate git form to react [EE-4849] (#8268)

This commit is contained in:
Chaim Lev-Ari 2023-02-23 01:43:33 +05:30 committed by GitHub
parent afe6cd6df0
commit 273a3f9a10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
130 changed files with 3194 additions and 1190 deletions

View file

@ -11,6 +11,7 @@ import servicesModule from './services';
import { reactModule } from './react';
import { sidebarModule } from './react/views/sidebar';
import environmentsModule from './environments';
import { helpersModule } from './helpers';
async function initAuthentication(authManager, Authentication, $rootScope, $state) {
authManager.checkAuthOnRefresh();
@ -41,6 +42,7 @@ angular
reactModule,
sidebarModule,
environmentsModule,
helpersModule,
])
.config([
'$stateRegistryProvider',

View file

@ -1,19 +0,0 @@
class GitFormAdditionalFileItemController {
onChangePath(value) {
const fieldIsInvalid = typeof value === 'undefined';
if (fieldIsInvalid) {
return;
}
this.onChange(this.index, { value });
}
removeValue() {
this.onChange(this.index);
}
$onInit() {
this.formName = `variableForm${this.index}`;
}
}
export default GitFormAdditionalFileItemController;

View file

@ -1,20 +0,0 @@
<ng-form class="env-item form-horizontal" name="$ctrl.{{ $ctrl.formName }}">
<div class="form-group col-sm-12">
<div class="form-inline mt-3">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon required">path</span>
<input type="text" name="name" class="form-control" ng-model="$ctrl.variable" ng-change="$ctrl.onChangePath($ctrl.variable)" required />
</div>
<button class="btn btn-dangerlight" type="button" ng-click="$ctrl.removeValue()" title="Remove">
<pr-icon icon="'trash-2'" size="'md'"></pr-icon>
</button>
</div>
<div ng-show="$ctrl[$ctrl.formName].name.$invalid">
<div class="small text-warning">
<div ng-messages="$ctrl[$ctrl.formName].name.$error" class="mt-1">
<p class="vertical-center" ng-message="required"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Path is required. </p>
</div>
</div>
</div>
</div>
</ng-form>

View file

@ -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: '<',
},
};

View file

@ -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;

View file

@ -1,18 +0,0 @@
<div class="form-group">
<div class="col-sm-12 p-0">
<div class="col-sm-3 col-lg-2">
<label class="control-label text-left">Additional paths</label>
</div>
<div class="col-sm-9 pt-1">
<span class="label label-default interactive vertical-center" ng-click="$ctrl.add()"> <pr-icon icon="'plus'" size="'sm'" mode="'alt'"></pr-icon> <span>add file</span> </span>
</div>
</div>
<div class="col-sm-12 form-inline">
<git-form-additional-file-item
ng-repeat="variable in $ctrl.model.AdditionalFiles track by $index"
variable="variable"
index="$index"
on-change="($ctrl.onChangeVariable)"
></git-form-additional-file-item>
</div>
</div>

View file

@ -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: '<',
},
};

View file

@ -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<GitAuthModel> = {};
$async: <T>(fn: () => Promise<T>) => Promise<T>;
gitFormAuthFieldset?: IFormController;
gitCredentials: Array<GitCredential> = [];
Authentication: IAuthenticationService;
value?: GitAuthModel;
onChange?: (value: GitAuthModel) => void;
/* @ngInject */
constructor(
$async: <T>(fn: () => Promise<T>) => Promise<T>,
Authentication: IAuthenticationService
) {
this.$async = $async;
this.Authentication = Authentication;
this.handleChange = this.handleChange.bind(this);
this.runGitValidation = this.runGitValidation.bind(this);
}
async handleChange(newValues: Partial<GitAuthModel>) {
// 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<GitAuthModel>(
() => 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);
}
}

View file

@ -0,0 +1,21 @@
import { IComponentOptions } from 'angular';
import controller from './git-form-auth-fieldset.controller';
export const gitFormAuthFieldset: IComponentOptions = {
controller,
template: `
<ng-form name="$ctrl.gitFormAuthFieldset">
<react-git-form-auth-fieldset
value="$ctrl.value"
on-change="$ctrl.handleChange"
is-explanation-visible="$ctrl.isExplanationVisible"
errors="$ctrl.errors">
</react-git-form-auth-fieldset>
</ng-form>`,
bindings: {
value: '<',
onChange: '<',
isExplanationVisible: '<',
},
};

View file

@ -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;

View file

@ -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);
}

View file

@ -1,51 +0,0 @@
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
checked="$ctrl.model.RepositoryAuthentication"
label="'Authentication'"
label-class="'col-sm-3 col-lg-2'"
name="'authSwitch'"
on-change="($ctrl.onChangeAuth)"
data-cy="'component-gitAuthToggle'"
switch-values="{on:'Yes',off:'No'}"
></por-switch-field>
</div>
</div>
<div class="small mt-1 mb-3" ng-if="$ctrl.model.RepositoryAuthentication && $ctrl.showAuthExplanation">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
<span class="text-muted">Enabling authentication will store the credentials and it is advisable to use a git service account</span>
</div>
<div ng-if="$ctrl.model.RepositoryAuthentication" class="row">
<div class="form-group">
<label for="repository_username" class="col-lg-2 col-sm-3 control-label required text-left"> Username </label>
<div class="col-sm-8">
<input
type="text"
class="form-control"
ng-model="$ctrl.model.RepositoryUsername"
name="repository_username"
placeholder="git username"
ng-change="$ctrl.onChangeUsername($ctrl.model.RepositoryUsername)"
data-cy="component-gitUsernameInput"
/>
</div>
</div>
<div class="form-group flex">
<label for="repository_password" class="col-lg-2 col-sm-3 control-label !pt-0 text-left">
<div class="required"> Personal Access Token </div>
<portainer-tooltip message="'Provide a personal access token or password'"></portainer-tooltip>
</label>
<div class="col-sm-8">
<input
type="password"
class="form-control"
ng-model="$ctrl.model.RepositoryPassword"
name="repository_password"
placeholder="*******"
ng-change="$ctrl.onChangePassword($ctrl.model.RepositoryPassword)"
ng-required="!$ctrl.isEdit"
data-cy="component-gitPasswordInput"
/>
</div>
</div>
</div>

View file

@ -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: '<',
},
};

View file

@ -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<AutoUpdateModel> = {};
$async: <T>(fn: () => Promise<T>) => Promise<T>;
gitFormAutoUpdate?: IFormController;
Authentication: IAuthenticationService;
value?: AutoUpdateModel;
onChange?: (value: AutoUpdateModel) => void;
/* @ngInject */
constructor(
$async: <T>(fn: () => Promise<T>) => Promise<T>,
Authentication: IAuthenticationService
) {
this.$async = $async;
this.Authentication = Authentication;
this.handleChange = this.handleChange.bind(this);
this.runGitValidation = this.runGitValidation.bind(this);
}
async handleChange(newValues: Partial<AutoUpdateModel>) {
// 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<AutoUpdateModel>(
() => 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);
}
}

View file

@ -0,0 +1,24 @@
import { IComponentOptions } from 'angular';
import controller from './git-form-auto-update-fieldset.controller';
export const gitFormAutoUpdate: IComponentOptions = {
template: `<ng-form name="$ctrl.gitFormAutoUpdate">
<react-git-form-auto-update-fieldset
value="$ctrl.value"
on-change="$ctrl.handleChange"
environment-type="$ctrl.environmentType"
is-force-pull-visible="$ctrl.isForcePullVisible"
base-webhook-url="$ctrl.baseWebhookUrl"
errors="$ctrl.errors">
</react-git-form-auto-update-fieldset>
</ng-form>`,
bindings: {
value: '<',
onChange: '<',
environmentType: '@',
isForcePullVisible: '<',
baseWebhookUrl: '@',
},
controller,
};

View file

@ -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;

View file

@ -1,122 +0,0 @@
<ng-form name="autoUpdateForm" class="form-group">
<div class="small vertical-center mb-2">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
<span class="text-muted">
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.</span
>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
name="'autoUpdate'"
checked="$ctrl.model.RepositoryAutomaticUpdates"
label="'Automatic updates'"
label-class="'col-sm-3 col-lg-2'"
on-change="($ctrl.onChangeAutoUpdate)"
switch-values="{on:'Yes',off:'No'}"
></por-switch-field>
</div>
</div>
<div class="small vertical-center" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
<span class="text-muted"
>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.</span
>
</div>
<div class="form-group mt-2" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
<label for="repository_mechanism" class="col-lg-2 col-sm-3 control-label text-left"> Mechanism </label>
<div class="col-sm-8">
<div class="input-group col-sm-10 input-group-sm">
<div class="btn-group btn-group-sm">
<label class="btn btn-light" ng-click="$ctrl.onChangeMechanism($ctrl.model.RepositoryMechanism)" ng-model="$ctrl.model.RepositoryMechanism" uib-btn-radio="'Interval'"
>Polling</label
>
<label class="btn btn-light" ng-click="$ctrl.onChangeMechanism($ctrl.model.RepositoryMechanism)" ng-model="$ctrl.model.RepositoryMechanism" uib-btn-radio="'Webhook'"
>Webhook</label
>
</div>
</div>
</div>
</div>
<div class="form-group" ng-if="$ctrl.model.RepositoryAutomaticUpdates && $ctrl.model.RepositoryMechanism === 'Webhook'">
<label for="repository_mechanism" class="col-sm-3 col-lg-2 control-label text-left">
Webhook
<portainer-tooltip
message="$ctrl.environmentType === 'KUBERNETES' ?
'See&nbsp;<a href=\'https://docs.portainer.io/user/kubernetes/applications/webhooks\' target=\'_blank\' rel=\'noreferrer\'>Portainer documentation on webhook usage</a>.' :
'See&nbsp;<a href=\'https://docs.portainer.io/user/docker/stacks/webhooks\' target=\'_blank\' rel=\'noreferrer\'>Portainer documentation on webhook usage</a>.'"
set-html-message="true"
></portainer-tooltip>
</label>
<div class="col-sm-8">
<span class="text-muted"> {{ $ctrl.model.RepositoryWebhookURL | truncatelr }} </span>
<button type="button" class="btn btn-sm btn-light btn-sm space-left vertical-center" ng-if="$ctrl.model.RepositoryWebhookURL" ng-click="$ctrl.copyWebhook()">
<pr-icon icon="'copy'" size="'sm'"></pr-icon> Copy link
</button>
<span>
<pr-icon icon="'check'" mode="'success'" style="display: none"></pr-icon>
</span>
</div>
</div>
<div class="form-group" ng-if="$ctrl.model.RepositoryAutomaticUpdates && $ctrl.model.RepositoryMechanism === 'Interval'">
<label for="repository_fetch_interval" class="col-sm-3 col-lg-2 control-label required text-left">
Fetch interval
<portainer-tooltip
message="'Specify how frequently polling occurs using syntax such as, 5m = 5 minutes, 24h = 24 hours, 6h40m = 6 hours and 40 minutes.'"
></portainer-tooltip>
</label>
<div class="col-sm-8">
<input
type="text"
class="form-control"
ng-change="$ctrl.onChangeInterval($ctrl.model.RepositoryFetchInterval)"
ng-model="$ctrl.model.RepositoryFetchInterval"
name="repository_fetch_interval"
placeholder="5m"
required
interval-format
/>
<div class="help-group">
<div class="form-group col-md-12 pt-1" ng-show="autoUpdateForm.repository_fetch_interval.$touched && autoUpdateForm.repository_fetch_interval.$invalid">
<div class="small text-warning">
<div ng-messages="autoUpdateForm.repository_fetch_interval.$error">
<p ng-message="required"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This field is required.</p>
<p ng-message="invalidIntervalFormat"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Please enter a valid time interval.</p>
<p ng-message="minimumInterval"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Minimum interval is 1m</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="form-group" ng-if="$ctrl.showForcePullImage && $ctrl.model.RepositoryAutomaticUpdates">
<div class="col-sm-12">
<por-switch-field
name="'forcePullImage'"
feature-id="$ctrl.stackPullImageFeature"
checked="$ctrl.model.ForcePullImage"
label="'Re-pull image'"
label-class="'col-sm-3 col-lg-2'"
tooltip="'If enabled, then when redeploy is triggered via the webhook or polling, if there\'s a newer image with the tag that you\'ve specified (e.g. changeable development builds), it\'s pulled and redeployed. If you haven\'t specified a tag, or have specified \'latest\' as the tag, then the image with the tag \'latest\' is pulled and redeployed.'"
></por-switch-field>
</div>
</div>
<div class="form-group" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
<div class="col-sm-12">
<por-switch-field
name="'forceUpdate'"
feature-id="$ctrl.limitedFeature"
checked="$ctrl.model.RepositoryAutomaticUpdatesForce"
label="$ctrl.environmentType === 'KUBERNETES' ? 'Always apply manifest' : 'Force redeployment'"
tooltip="$ctrl.environmentType === 'KUBERNETES' ? '<p>If enabled, then when redeploy is triggered via the webhook or polling, kubectl apply is always performed, even if Portainer detects no difference between the git repo and what was stored locally on last git pull.</ p><p>This is useful if you want your git repo to be the source of truth and are fine with changes made directly to resources in the cluster being overwritten.</ p>' : ''"
set-tooltip-html-message="true"
label-class="'col-sm-3 col-lg-2'"
on-change="($ctrl.onChangeAutoUpdateForce)"
></por-switch-field>
</div>
</div>
</ng-form>

View file

@ -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: '<',
},
};

View file

@ -1,29 +0,0 @@
<ng-form name="pathForm">
<div class="form-group">
<span class="col-sm-12 text-muted small vertical-center">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
<span
>Indicate the path to the {{ $ctrl.deployMethod == 'compose' ? 'Compose' : 'Manifest' }} file from the root of your repository.
<span ng-if="$ctrl.isDockerStandalone">
To enable rebuilding of an image if already present on Docker standalone environments, include<code>pull_policy: build</code>in your compose file as per
<a href="https://docs.docker.com/compose/compose-file/#pull_policy">Docker documentation</a>.</span
></span
>
</span>
</div>
<div class="form-group">
<label for="stack_repository_path" class="col-lg-2 col-sm-3 control-label required text-left">{{ $ctrl.deployMethod == 'compose' ? 'Compose' : 'Manifest' }} path</label>
<div class="col-sm-8">
<input
type="text"
class="form-control"
name="repoPathField"
ng-model="$ctrl.value"
ng-change="$ctrl.onChange($ctrl.value)"
id="stack_repository_path"
placeholder="{{ $ctrl.deployMethod == 'compose' ? 'docker-compose.yml' : 'manifest.yml' }}"
required
/>
</div>
</div>
</ng-form>

View file

@ -1,9 +0,0 @@
export const gitFormComposePathField = {
templateUrl: './git-form-compose-path-field.html',
bindings: {
deployMethod: '@',
value: '<',
onChange: '<',
isDockerStandalone: '<',
},
};

View file

@ -1,15 +0,0 @@
<div class="form-group" ng-class="$ctrl.className">
<div class="col-sm-12">
<p>
This {{ $ctrl.type }} was deployed from the git repository <code>{{ $ctrl.url }}</code>
.
</p>
<p>
Update
<code
>{{ $ctrl.configFilePath }}<span ng-if="$ctrl.additionalFiles.length > 0">, {{ $ctrl.additionalFiles.join(',') }}</span></code
>
in git and pull from here to update the {{ $ctrl.type }}.
</p>
</div>
</div>

View file

@ -1,10 +0,0 @@
export const gitFormInfoPanel = {
templateUrl: './git-form-info-panel.html',
bindings: {
url: '<',
configFilePath: '<',
additionalFiles: '<',
className: '@',
type: '@',
},
};

View file

@ -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<string>(
() => refFieldValidation(),
value
);
if (this.error) {
this.gitFormRefField?.$setValidity(
'gitFormRefField',
false,
this.gitFormRefField
);
}
});
}
}
export const gitFormRefField: IComponentOptions = {
controller: GitFormRefFieldController,
template: `
<ng-form name="$ctrl.gitFormRefField">
<react-git-form-ref-field
is-url-valid="$ctrl.isUrlValid"
model="$ctrl.model"
value="$ctrl.value"
on-change="$ctrl.handleChange"
stack-id="$ctrl.stackId"
error="$ctrl.error">
</react-git-form-ref-field>
</ng-form>`,
bindings: {
isUrlValid: '<',
value: '<',
onChange: '<',
model: '<',
stackId: '<',
},
};

View file

@ -1,23 +0,0 @@
<div class="form-group">
<span class="col-sm-12 text-muted small vertical-center">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
<span>
Specify a reference of the repository using the following syntax: branches with <code>refs/heads/branch_name</code> or tags with <code>refs/tags/tag_name</code>. If not
specified, will use the default <code>HEAD</code> reference normally the <code>master</code> branch.
</span>
</span>
</div>
<div class="form-group">
<label for="stack_repository_reference_name" class="col-lg-2 col-sm-3 control-label text-left">Repository reference</label>
<div class="col-sm-8">
<input
type="text"
class="form-control"
ng-model="$ctrl.value"
id="stack_repository_reference_name"
placeholder="refs/heads/master"
ng-change="$ctrl.onChange($ctrl.value)"
data-cy="component-gitRefInput"
/>
</div>
</div>

View file

@ -1,7 +0,0 @@
export const gitFormRefField = {
templateUrl: './git-form-ref-field.html',
bindings: {
value: '<',
onChange: '<',
},
};

View file

@ -1,19 +0,0 @@
<div class="form-group">
<span class="col-sm-12 text-muted small"> You can use the URL of a git repository. </span>
</div>
<div class="form-group">
<label for="stack_repository_url" class="col-lg-2 col-sm-3 control-label required text-left">Repository URL</label>
<div class="col-sm-8">
<input
type="text"
name="repoUrlField"
class="form-control"
ng-model="$ctrl.value"
ng-change="$ctrl.onChange($ctrl.value)"
id="stack_repository_url"
placeholder="https://github.com/portainer/portainer-compose"
data-cy="component-gitUrlInput"
required
/>
</div>
</div>

View file

@ -1,7 +0,0 @@
export const gitFormUrlField = {
templateUrl: './git-form-url-field.html',
bindings: {
value: '<',
onChange: '<',
},
};

View file

@ -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';
}
}

View file

@ -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<GitFormModel>;
$async: <T>(fn: () => Promise<T>) => Promise<T>;
gitForm?: IFormController;
gitCredentials: Array<GitCredential> = [];
Authentication: IAuthenticationService;
value?: GitFormModel;
onChange?: (value: GitFormModel) => void;
/* @ngInject */
constructor(
$async: <T>(fn: () => Promise<T>) => Promise<T>,
Authentication: IAuthenticationService
) {
this.$async = $async;
this.Authentication = Authentication;
this.handleChange = this.handleChange.bind(this);
this.runGitFormValidation = this.runGitFormValidation.bind(this);
}
async handleChange(newValues: Partial<GitFormModel>) {
// 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);
}
}

View file

@ -1,24 +0,0 @@
<ng-form name="$ctrl.gitForm">
<div class="col-sm-12 form-section-title"> Git repository </div>
<git-form-auth-fieldset model="$ctrl.model" on-change="($ctrl.onChange)"></git-form-auth-fieldset>
<git-form-url-field value="$ctrl.model.RepositoryURL" on-change="($ctrl.onChangeURL)"></git-form-url-field>
<git-form-ref-field value="$ctrl.model.RepositoryReferenceName" on-change="($ctrl.onChangeRefName)"></git-form-ref-field>
<git-form-compose-path-field
value="$ctrl.model.ComposeFilePathInRepository"
on-change="($ctrl.onChangeComposePath)"
deploy-method="{{ $ctrl.deployMethod }}"
is-docker-standalone="$ctrl.isDockerStandalone"
></git-form-compose-path-field>
<git-form-additional-files-panel ng-if="$ctrl.additionalFile" model="$ctrl.model" on-change="($ctrl.onChange)"></git-form-additional-files-panel>
<git-form-auto-update-fieldset
ng-if="$ctrl.autoUpdate"
model="$ctrl.model"
on-change="($ctrl.onChange)"
show-force-pull-image="$ctrl.showForcePullImage"
></git-form-auto-update-fieldset>
</ng-form>

View file

@ -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: '@',
},
};

View file

@ -0,0 +1,33 @@
import { IComponentOptions } from 'angular';
import controller from './git-form.controller';
export const gitForm: IComponentOptions = {
template: `
<ng-form name="$ctrl.gitForm">
<react-git-form
value="$ctrl.value"
on-change="$ctrl.handleChange"
is-docker-standalone="$ctrl.isDockerStandalone"
deploy-method="$ctrl.deployMethod"
is-additional-files-field-visible="$ctrl.isAdditionalFilesFieldVisible"
is-auto-update-visible="$ctrl.isAutoUpdateVisible"
is-force-pull-visible="$ctrl.isForcePullVisible"
is-auth-explanation-visible="$ctrl.isAuthExplanationVisible"
base-webhook-url="$ctrl.baseWebhookUrl"
errors="$ctrl.errors">
</react-git-form>
</ng-form>`,
bindings: {
value: '<',
onChange: '<',
isDockerStandalone: '<',
deployMethod: '@',
baseWebhookUrl: '@',
isAdditionalFilesFieldVisible: '<',
isAutoUpdateVisible: '<',
isForcePullVisible: '<',
isAuthExplanationVisible: '<',
},
controller,
};

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -1,57 +0,0 @@
<form name="$ctrl.redeployGitForm">
<div class="col-sm-12 form-section-title"> Redeploy from git repository </div>
<div class="form-group text-muted">
<div class="col-sm-12">
<p> Pull the latest manifest from git and redeploy the application. </p>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<p>
<a class="small interactive" ng-click="$ctrl.state.showConfig = !$ctrl.state.showConfig">
<pr-icon ng-if="$ctrl.state.showConfig" icon="'minus'" class-name="'mr-1'"></pr-icon>
<pr-icon ng-if="!$ctrl.state.showConfig" icon="'plus'" class-name="'mr-1'"></pr-icon>
{{ $ctrl.state.showConfig ? 'Hide' : 'Advanced' }} configuration
</a>
</p>
</div>
</div>
<git-form-ref-field ng-if="$ctrl.state.showConfig" value="$ctrl.formValues.RefName" on-change="($ctrl.onChangeRef)"></git-form-ref-field>
<git-form-auth-fieldset
ng-if="$ctrl.state.showConfig"
model="$ctrl.formValues"
is-edit="$ctrl.state.isEdit"
on-change="($ctrl.onChange)"
show-auth-explanation="true"
></git-form-auth-fieldset>
<div class="col-sm-12 form-section-title"> Actions </div>
<!-- #Git buttons -->
<button
class="btn btn-sm btn-primary"
ng-click="$ctrl.pullAndRedeployApplication()"
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.redeployGitForm.$valid"
style="margin-top: 7px; margin-left: 0"
button-spinner="ctrl.state.redeployInProgress"
analytics-on
analytics-category="kubernetes"
analytics-event="kubernetes-application-edit-git-pull"
>
<span ng-show="!$ctrl.state.redeployInProgress">
<pr-icon icon="'refresh-cw'" class="!mr-1"></pr-icon>
Pull and update application
</span>
<span ng-show="$ctrl.state.redeployInProgress">In progress...</span>
</button>
<button
class="btn btn-sm btn-primary"
ng-click="$ctrl.saveGitSettings()"
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.redeployGitForm.$valid"
style="margin-top: 7px; margin-left: 0"
button-spinner="$ctrl.state.saveGitSettingsInProgress"
>
<span ng-show="!$ctrl.state.saveGitSettingsInProgress"> Save settings </span>
<span ng-show="$ctrl.state.saveGitSettingsInProgress">In progress...</span>
</button>
</form>

View file

@ -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);

View file

@ -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;

View file

@ -5,7 +5,14 @@
<p> Pull the latest manifest from git and redeploy the application. </p>
</div>
</div>
<git-form-auto-update-fieldset model="$ctrl.formValues.AutoUpdate" on-change="($ctrl.onChangeAutoUpdate)"></git-form-auto-update-fieldset>
<git-form-auto-update-fieldset
value="$ctrl.formValues.AutoUpdate"
on-change="($ctrl.onChangeAutoUpdate)"
environment-type="KUBERNETES"
base-webhook-url="{{ $ctrl.state.baseWebhookUrl }}"
></git-form-auto-update-fieldset>
<time-window-display></time-window-display>
<div class="form-group">
<div class="col-sm-12">
<p>
@ -17,14 +24,14 @@
</p>
</div>
</div>
<git-form-ref-field ng-if="$ctrl.state.showConfig" value="$ctrl.formValues.RefName" on-change="($ctrl.onChangeRef)"></git-form-ref-field>
<git-form-auth-fieldset
<git-form-ref-field
ng-if="$ctrl.state.showConfig"
value="$ctrl.formValues.RefName"
on-change="($ctrl.onChangeRef)"
model="$ctrl.formValues"
is-edit="$ctrl.state.isEdit"
on-change="($ctrl.onChange)"
show-auth-explanation="true"
></git-form-auth-fieldset>
is-url-valid="true"
></git-form-ref-field>
<div class="col-sm-12 form-section-title"> Actions </div>
<!-- #Git buttons -->

View file

@ -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;

View file

@ -1,17 +1,19 @@
<form name="$ctrl.redeployGitForm" class="form-horizontal my-8">
<div class="col-sm-12 form-section-title"> Redeploy from git repository </div>
<git-form-info-panel
class-name="text-muted small"
class-name="'text-muted small'"
url="$ctrl.model.URL"
type="stack"
type="'stack'"
config-file-path="$ctrl.model.ConfigFilePath"
additional-files="$ctrl.stack.AdditionalFiles"
></git-form-info-panel>
<git-form-auto-update-fieldset
model="$ctrl.formValues.AutoUpdate"
value="$ctrl.formValues.AutoUpdate"
on-change="($ctrl.onChangeAutoUpdate)"
show-force-pull-image="$ctrl.stack.Type !== 3"
environment-type="DOCKER"
is-force-pull-visible="$ctrl.stack.Type !== 3"
base-webhook-url="{{ $ctrl.state.baseWebhookUrl }}"
></git-form-auto-update-fieldset>
<div class="form-group">
<div class="col-sm-12">
@ -24,14 +26,17 @@
</p>
</div>
</div>
<git-form-ref-field ng-if="$ctrl.state.showConfig" value="$ctrl.formValues.RefName" on-change="($ctrl.onChangeRef)"></git-form-ref-field>
<git-form-auth-fieldset
<git-form-ref-field
ng-if="$ctrl.state.showConfig"
is-edit="$ctrl.state.isEdit"
value="$ctrl.formValues.RefName"
on-change="($ctrl.onChangeRef)"
model="$ctrl.formValues"
on-change="($ctrl.onChange)"
show-auth-explanation="true"
></git-form-auth-fieldset>
is-url-valid="true"
stack-id="$ctrl.gitStackId"
></git-form-ref-field>
<git-form-auth-fieldset ng-if="$ctrl.state.showConfig" value="$ctrl.formValues" on-change="($ctrl.onChangeGitAuth)" is-auth-explanation-visible="true"></git-form-auth-fieldset>
<environment-variables-panel
ng-model="$ctrl.formValues.Env"
explanation="These values will be used as substitutions in the stack file"
@ -39,6 +44,9 @@
show-help-message="true"
></environment-variables-panel>
<option-panel ng-if="$ctrl.stack.Type === 1 && $ctrl.endpoint.apiVersion >= 1.27" ng-model="$ctrl.formValues.Option" on-change="($ctrl.onChangeOption)"></option-panel>
<div class="col-sm-12 form-section-title"> Actions </div>
<button
class="btn btn-sm btn-primary"
ng-click="$ctrl.submit()"

View file

@ -1,13 +1,13 @@
import angular from 'angular';
import formComponentsModule from './form-components';
import gitFormModule from './forms/git-form';
import porAccessManagementModule from './accessManagement';
import widgetModule from './widget';
import { boxSelectorModule } from './BoxSelector';
import { beFeatureIndicator } from './BEFeatureIndicator';
import { InformationPanelAngular } from './InformationPanel';
import { gitFormModule } from './forms/git-form';
export default angular
.module('portainer.app.components', [boxSelectorModule, widgetModule, gitFormModule, porAccessManagementModule, formComponentsModule])

View file

@ -1,8 +1,19 @@
export default class PortainerError extends Error {
err?: Error;
isPortainerError = true;
constructor(msg: string, err?: Error) {
super(msg);
this.err = err;
}
}
export function isPortainerError(error: unknown): error is PortainerError {
return (
!!error &&
typeof error === 'object' &&
'isPortainerError' in error &&
(error as PortainerError).isPortainerError
);
}

View file

@ -0,0 +1,7 @@
import angular from 'angular';
import { WebhookHelperFactory } from './webhookHelper';
export const helpersModule = angular
.module('portainer.app.helpers', [])
.factory('WebhookHelper', WebhookHelperFactory).name;

View file

@ -6,5 +6,5 @@
*/
export function baseHref() {
const base = document.getElementById('base');
return base ? base.getAttribute('href') : '/';
return base ? base.getAttribute('href') || '/' : '/';
}

View file

@ -1,33 +0,0 @@
import { baseHref } from '@/portainer/helpers/pathHelper';
angular.module('portainer.app').factory('WebhookHelper', [
'$location',
'API_ENDPOINT_WEBHOOKS',
'API_ENDPOINT_STACKS',
function WebhookHelperFactory($location, API_ENDPOINT_WEBHOOKS, API_ENDPOINT_STACKS) {
'use strict';
var helper = {};
let base;
const protocol = $location.protocol().toLowerCase();
if (protocol !== 'file') {
const host = $location.host();
const port = $location.port();
const displayPort = (protocol === 'http' && port === 80) || (protocol === 'https' && port === 443) ? '' : ':' + port;
base = `${protocol}://${host}${displayPort}${baseHref()}`;
} else {
base = baseHref();
}
helper.returnWebhookUrl = function (token) {
return `${base}${API_ENDPOINT_WEBHOOKS}/${token}`;
};
helper.returnStackWebhookUrl = function (token) {
return `${base}${API_ENDPOINT_STACKS}/webhooks/${token}`;
};
return helper;
},
]);

View file

@ -0,0 +1,43 @@
import { API_ENDPOINT_STACKS, API_ENDPOINT_WEBHOOKS } from '@/constants';
import { baseHref } from './pathHelper';
const baseUrl = getBaseUrl();
export function dockerWebhookUrl(token: string) {
return `${baseUrl}${API_ENDPOINT_WEBHOOKS}/${token}`;
}
export function baseStackWebhookUrl() {
return `${baseUrl}${API_ENDPOINT_STACKS}/webhooks`;
}
export function stackWebhookUrl(token: string) {
return `${baseStackWebhookUrl()}/${token}`;
}
/* @ngInject */
export function WebhookHelperFactory() {
return {
returnWebhookUrl: dockerWebhookUrl,
getBaseStackWebhookUrl: baseStackWebhookUrl,
returnStackWebhookUrl: stackWebhookUrl,
};
}
function getBaseUrl() {
const protocol = window.location.protocol.toLowerCase().replace(':', '');
if (protocol === 'file') {
return baseHref();
}
const { hostname } = window.location;
const port = parseInt(window.location.port, 10);
const displayPort =
(protocol === 'http' && port === 80) ||
(protocol === 'https' && port === 443)
? ''
: `:${port}`;
return `${protocol}://${hostname}${displayPort}${baseHref()}`;
}

View file

@ -3,6 +3,8 @@ export function UserViewModel(data) {
this.Username = data.Username;
this.Role = data.Role;
this.ThemeSettings = data.ThemeSettings;
this.EndpointAuthorizations = data.EndpointAuthorizations;
this.PortainerAuthorizations = data.PortainerAuthorizations;
if (data.Role === 1) {
this.RoleName = 'administrator';
} else {
@ -10,8 +12,6 @@ export function UserViewModel(data) {
}
this.AuthenticationMethod = data.AuthenticationMethod;
this.Checked = false;
this.EndpointAuthorizations = null;
this.PortainerAuthorizations = null;
}
export function UserTokenModel(data) {

View file

@ -0,0 +1,69 @@
import angular from 'angular';
import { r2a } from '@/react-tools/react2angular';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { withReactQuery } from '@/react-tools/withReactQuery';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { AutoUpdateFieldset } from '@/react/portainer/gitops/AutoUpdateFieldset';
import { GitForm } from '@/react/portainer/gitops/GitForm';
import { AuthFieldset } from '@/react/portainer/gitops/AuthFieldset';
import { InfoPanel } from '@/react/portainer/gitops/InfoPanel';
import { RefField } from '@/react/portainer/gitops/RefField';
export const gitFormModule = angular
.module('portainer.app.components.forms.git', [])
.component(
'reactGitForm',
r2a(withUIRouter(withReactQuery(withCurrentUser(GitForm))), [
'value',
'onChange',
'isDockerStandalone',
'deployMethod',
'isAdditionalFilesFieldVisible',
'isForcePullVisible',
'isAuthExplanationVisible',
'errors',
'baseWebhookUrl',
])
)
.component(
'gitFormInfoPanel',
r2a(InfoPanel, [
'additionalFiles',
'className',
'configFilePath',
'type',
'url',
])
)
.component(
'reactGitFormAutoUpdateFieldset',
r2a(AutoUpdateFieldset, [
'value',
'onChange',
'environmentType',
'isForcePullVisible',
'errors',
'baseWebhookUrl',
])
)
.component(
'reactGitFormAuthFieldset',
r2a(withUIRouter(withReactQuery(withCurrentUser(AuthFieldset))), [
'value',
'isExplanationVisible',
'onChange',
'errors',
])
)
.component(
'reactGitFormRefField',
r2a(withUIRouter(withReactQuery(withCurrentUser(RefField))), [
'error',
'model',
'onChange',
'stackId',
'value',
'isUrlValid',
])
).name;

View file

@ -38,13 +38,18 @@ import { PortainerSelect } from '@@/form-components/PortainerSelect';
import { Slider } from '@@/form-components/Slider';
import { TagButton } from '@@/TagButton';
import { BETeaserButton } from '@@/BETeaserButton';
import { TimeWindowDisplay } from '@@/TimeWindowDisplay';
import { fileUploadField } from './file-upload-field';
import { switchField } from './switch-field';
import { customTemplatesModule } from './custom-templates';
import { gitFormModule } from './git-form';
export const componentsModule = angular
.module('portainer.app.react.components', [customTemplatesModule])
.module('portainer.app.react.components', [
customTemplatesModule,
gitFormModule,
])
.component(
'tagSelector',
r2a(withUIRouter(withReactQuery(TagSelector)), [
@ -232,4 +237,8 @@ export const componentsModule = angular
'porAccessManagementUsersSelector',
r2a(PorAccessManagementUsersSelector, ['onChange', 'options', 'value'])
)
.component('edgeKeyDisplay', r2a(EdgeKeyDisplay, ['edgeKey'])).name;
.component('edgeKeyDisplay', r2a(EdgeKeyDisplay, ['edgeKey']))
.component(
'timeWindowDisplay',
r2a(withReactQuery(withUIRouter(TimeWindowDisplay)), [])
).name;

View file

@ -1,5 +1,5 @@
import _ from 'lodash-es';
import { RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
import { transformAutoUpdateViewModel } from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
import { StackViewModel, OrphanedStackViewModel } from '../../models/stack';
angular.module('portainer.app').factory('StackService', [
@ -278,17 +278,8 @@ angular.module('portainer.app').factory('StackService', [
StackFileContent: stackFile,
};
} else {
const autoUpdate = {};
if (gitConfig.AutoUpdate && gitConfig.AutoUpdate.RepositoryAutomaticUpdates) {
if (gitConfig.AutoUpdate.RepositoryMechanism === RepositoryMechanismTypes.INTERVAL) {
autoUpdate.Interval = gitConfig.AutoUpdate.RepositoryFetchInterval;
} else if (gitConfig.AutoUpdate.RepositoryMechanism === RepositoryMechanismTypes.WEBHOOK) {
autoUpdate.Webhook = gitConfig.AutoUpdate.RepositoryWebhookURL.split('/').reverse()[0];
}
}
payload = {
AutoUpdate: autoUpdate,
AutoUpdate: transformAutoUpdateViewModel(gitConfig.AutoUpdate),
RepositoryReferenceName: gitConfig.RefName,
RepositoryAuthentication: gitConfig.RepositoryAuthentication,
RepositoryUsername: gitConfig.RepositoryUsername,
@ -465,21 +456,10 @@ angular.module('portainer.app').factory('StackService', [
}
service.updateGitStackSettings = function (id, endpointId, env, gitConfig) {
// prepare auto update
const autoUpdate = {};
if (gitConfig.AutoUpdate.RepositoryAutomaticUpdates) {
if (gitConfig.AutoUpdate.RepositoryMechanism === RepositoryMechanismTypes.INTERVAL) {
autoUpdate.Interval = gitConfig.AutoUpdate.RepositoryFetchInterval;
} else if (gitConfig.AutoUpdate.RepositoryMechanism === RepositoryMechanismTypes.WEBHOOK) {
autoUpdate.Webhook = gitConfig.AutoUpdate.RepositoryWebhookURL.split('/').reverse()[0];
}
}
return Stack.updateGitStackSettings(
{ endpointId, id },
{
AutoUpdate: autoUpdate,
AutoUpdate: transformAutoUpdateViewModel(gitConfig.AutoUpdate),
Env: env,
RepositoryReferenceName: gitConfig.RefName,
RepositoryAuthentication: gitConfig.RepositoryAuthentication,

View file

@ -3,3 +3,9 @@ import { Environment } from '@/react/portainer/environments/types';
export interface StateManager {
updateEndpointState(endpoint: Environment): Promise<void>;
}
export interface IAuthenticationService {
getUserDetails(): { ID: number };
}
export type AsyncService = <T>(fn: () => Promise<T>) => Promise<T>;

View file

@ -0,0 +1,155 @@
import { useMutation, useQuery, useQueryClient } from 'react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { success as notifySuccess } from '@/portainer/services/notifications';
import {
CreateGitCredentialPayload,
GitCredential,
UpdateGitCredentialPayload,
} from './types';
export async function createGitCredential(
gitCredential: CreateGitCredentialPayload
) {
try {
await axios.post(buildGitUrl(gitCredential.userId), gitCredential);
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to create git credential');
}
}
export async function getGitCredentials(userId: number) {
try {
const { data } = await axios.get<GitCredential[]>(buildGitUrl(userId));
return data;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to get git credentials');
}
}
export async function getGitCredential(userId: number, id: number) {
try {
const { data } = await axios.get<GitCredential>(buildGitUrl(userId, id));
return data;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to get git credential');
}
}
export async function deleteGitCredential(credential: GitCredential) {
try {
await axios.delete<GitCredential[]>(
buildGitUrl(credential.userId, credential.id)
);
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to delete git credential');
}
}
export async function updateGitCredential(
credential: Partial<UpdateGitCredentialPayload>,
userId: number,
id: number
) {
try {
const { data } = await axios.put(buildGitUrl(userId, id), credential);
return data;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to update credential');
}
}
export function useUpdateGitCredentialMutation() {
const queryClient = useQueryClient();
return useMutation(
({
credential,
userId,
id,
}: {
credential: UpdateGitCredentialPayload;
userId: number;
id: number;
}) => updateGitCredential(credential, userId, id),
{
onSuccess: (_, data) => {
notifySuccess(
'Git credential updated successfully',
data.credential.name
);
return queryClient.invalidateQueries(['gitcredentials']);
},
meta: {
error: {
title: 'Failure',
message: 'Unable to update credential',
},
},
}
);
}
export function useDeleteGitCredentialMutation() {
const queryClient = useQueryClient();
return useMutation(deleteGitCredential, {
onSuccess: (_, credential) => {
notifySuccess('Git Credential deleted successfully', credential.name);
return queryClient.invalidateQueries(['gitcredentials']);
},
meta: {
error: {
title: 'Failure',
message: 'Unable to delete git credential',
},
},
});
}
export function useGitCredentials(userId: number) {
return useQuery('gitcredentials', () => getGitCredentials(userId), {
staleTime: 20,
meta: {
error: {
title: 'Failure',
message: 'Unable to retrieve git credentials',
},
},
});
}
export function useGitCredential(userId: number, id: number) {
return useQuery(['gitcredentials', id], () => getGitCredential(userId, id), {
meta: {
error: {
title: 'Failure',
message: 'Unable to retrieve git credential',
},
},
});
}
export function useCreateGitCredentialMutation() {
const queryClient = useQueryClient();
return useMutation(createGitCredential, {
onSuccess: (_, payload) => {
notifySuccess('Credentials created successfully', payload.name);
return queryClient.invalidateQueries(['gitcredentials']);
},
meta: {
error: {
title: 'Failure',
message: 'Unable to create credential',
},
},
});
}
function buildGitUrl(userId: number, credentialId?: number) {
return credentialId
? `/users/${userId}/gitcredentials/${credentialId}`
: `/users/${userId}/gitcredentials`;
}

View file

@ -0,0 +1,35 @@
import {
PaginationTableSettings,
SortableTableSettings,
} from '@@/datatables/types';
export interface GitCredentialTableSettings
extends SortableTableSettings,
PaginationTableSettings {}
export interface GitCredentialFormValues {
name: string;
username?: string;
password: string;
}
export interface CreateGitCredentialPayload {
userId: number;
name: string;
username?: string;
password: string;
}
export interface UpdateGitCredentialPayload {
name: string;
username?: string;
password: string;
}
export type GitCredential = {
id: number;
userId: number;
name: string;
username: string;
creationDate: number;
};

View file

@ -62,7 +62,12 @@
</div>
<!-- !upload -->
<!-- repository -->
<git-form ng-if="$ctrl.state.Method === 'repository'" model="$ctrl.formValues" on-change="($ctrl.handleChange)"></git-form>
<git-form
ng-if="$ctrl.state.Method === 'repository'"
value="$ctrl.formValues"
on-change="($ctrl.handleChange)"
is-docker-standalone="$ctrl.isDockerStandalone"
></git-form>
<div class="form-group" ng-if="!$ctrl.state.isTemplateValid">
<div class="col-sm-12">

View file

@ -206,6 +206,7 @@ class CreateCustomTemplateViewController {
this.state.endpointMode = applicationState.endpoint.mode;
let stackType = 0;
if (this.state.endpointMode.provider === 'DOCKER_STANDALONE') {
this.isDockerStandalone = true;
stackType = 2;
} else if (this.state.endpointMode.provider === 'DOCKER_SWARM_MODE') {
stackType = 1;

View file

@ -1,5 +1,4 @@
import angular from 'angular';
import uuidv4 from 'uuid/v4';
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
import { STACK_NAME_VALIDATION_REGEX } from '@/constants';
@ -9,6 +8,8 @@ import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { renderTemplate } from '@/react/portainer/custom-templates/components/utils';
import { editor, upload, git, customTemplate } from '@@/BoxSelector/common-options/build-methods';
import { confirmWebEditorDiscard } from '@@/modals/confirm';
import { parseAutoUpdateResponse, transformAutoUpdateViewModel } from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
import { baseStackWebhookUrl } from '@/portainer/helpers/webhookHelper';
angular
.module('portainer.app')
@ -29,8 +30,6 @@ angular
ContainerHelper,
CustomTemplateService,
ContainerService,
WebhookHelper,
clipboard,
endpoint
) {
$scope.onChangeTemplateId = onChangeTemplateId;
@ -55,11 +54,9 @@ angular
AdditionalFiles: [],
ComposeFilePathInRepository: 'docker-compose.yml',
AccessControlData: new AccessControlFormData(),
RepositoryAutomaticUpdates: false,
RepositoryMechanism: RepositoryMechanismTypes.INTERVAL,
RepositoryFetchInterval: '5m',
RepositoryWebhookURL: WebhookHelper.returnStackWebhookUrl(uuidv4()),
EnableWebhook: false,
Variables: {},
AutoUpdate: parseAutoUpdateResponse(),
};
$scope.state = {
@ -72,6 +69,7 @@ angular
isEditorDirty: false,
selectedTemplate: null,
selectedTemplateId: null,
baseWebhookUrl: baseStackWebhookUrl(),
};
$window.onbeforeunload = () => {
@ -99,14 +97,6 @@ angular
});
};
$scope.addAdditionalFiles = function () {
$scope.formValues.AdditionalFiles.push('');
};
$scope.removeAdditionalFiles = function (index) {
$scope.formValues.AdditionalFiles.splice(index, 1);
};
function buildAnalyticsProperties() {
const metadata = { type: methodLabel($scope.state.Method) };
@ -163,7 +153,6 @@ angular
function createSwarmStack(name, method) {
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
const endpointId = +$state.params.endpointId;
if (method === 'template' || method === 'editor') {
var stackFileContent = $scope.formValues.StackFileContent;
return StackService.createSwarmStackFromFileContent(name, stackFileContent, env, endpointId);
@ -183,25 +172,13 @@ angular
RepositoryAuthentication: $scope.formValues.RepositoryAuthentication,
RepositoryUsername: $scope.formValues.RepositoryUsername,
RepositoryPassword: $scope.formValues.RepositoryPassword,
AutoUpdate: transformAutoUpdateViewModel($scope.formValues.AutoUpdate),
};
getAutoUpdatesProperty(repositoryOptions);
return StackService.createSwarmStackFromGitRepository(name, repositoryOptions, env, endpointId);
}
}
function getAutoUpdatesProperty(repositoryOptions) {
if ($scope.formValues.RepositoryAutomaticUpdates) {
repositoryOptions.AutoUpdate = {};
if ($scope.formValues.RepositoryMechanism === RepositoryMechanismTypes.INTERVAL) {
repositoryOptions.AutoUpdate.Interval = $scope.formValues.RepositoryFetchInterval;
} else if ($scope.formValues.RepositoryMechanism === RepositoryMechanismTypes.WEBHOOK) {
repositoryOptions.AutoUpdate.Webhook = $scope.formValues.RepositoryWebhookURL.split('/').reverse()[0];
}
}
}
function createComposeStack(name, method) {
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
const endpointId = +$state.params.endpointId;
@ -221,20 +198,13 @@ angular
RepositoryAuthentication: $scope.formValues.RepositoryAuthentication,
RepositoryUsername: $scope.formValues.RepositoryUsername,
RepositoryPassword: $scope.formValues.RepositoryPassword,
AutoUpdate: transformAutoUpdateViewModel($scope.formValues.AutoUpdate),
};
getAutoUpdatesProperty(repositoryOptions);
return StackService.createComposeStackFromGitRepository(name, repositoryOptions, env, endpointId);
}
}
$scope.copyWebhook = function () {
clipboard.copyText($scope.formValues.RepositoryWebhookURL);
$('#copyNotification').show();
$('#copyNotification').fadeOut(2000);
};
$scope.handleEnvVarChange = handleEnvVarChange;
function handleEnvVarChange(value) {
$scope.formValues.Env = value;
@ -348,6 +318,7 @@ angular
async function initView() {
var endpointMode = $scope.applicationState.endpoint.mode;
$scope.state.StackType = 2;
$scope.isDockerStandalone = endpointMode.provider === 'DOCKER_STANDALONE';
if (endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER') {
$scope.state.StackType = 1;
}
@ -369,11 +340,13 @@ angular
initView();
function onChangeFormValues(values) {
$scope.formValues = {
...$scope.formValues,
...values,
};
function onChangeFormValues(newValues) {
return $async(async () => {
$scope.formValues = {
...$scope.formValues,
...newValues,
};
});
}
}
);

View file

@ -79,14 +79,14 @@
<!-- !upload -->
<git-form
ng-if="state.Method === 'repository'"
model="formValues"
value="formValues"
on-change="(onChangeFormValues)"
additional-file="true"
auto-update="true"
show-force-pull-image="true"
show-auth-explanation="true"
path-text-title="Compose path"
path-placeholder="docker-compose.yml"
is-docker-standalone="isDockerStandalone"
is-additional-files-field-visible="true"
is-auto-update-visible="true"
is-auth-explanation-visible="true"
is-force-pull-visible="true"
base-webhook-url="{{ state.baseWebhookUrl }}"
></git-form>
<div ng-show="state.Method === 'template'">