mirror of https://github.com/portainer/portainer
refactor(gitops): migrate git form to react [EE-4849] (#8268)
parent
afe6cd6df0
commit
273a3f9a10
@ -1,66 +0,0 @@
|
||||
import { BROWSER_OS_PLATFORM } from './react/constants';
|
||||
|
||||
export const API_ENDPOINT_AUTH = 'api/auth';
|
||||
export const API_ENDPOINT_BACKUP = 'api/backup';
|
||||
export const API_ENDPOINT_CUSTOM_TEMPLATES = 'api/custom_templates';
|
||||
export const API_ENDPOINT_EDGE_GROUPS = 'api/edge_groups';
|
||||
export const API_ENDPOINT_EDGE_JOBS = 'api/edge_jobs';
|
||||
export const API_ENDPOINT_EDGE_STACKS = 'api/edge_stacks';
|
||||
export const API_ENDPOINT_EDGE_TEMPLATES = 'api/edge_templates';
|
||||
export const API_ENDPOINT_ENDPOINTS = 'api/endpoints';
|
||||
export const API_ENDPOINT_ENDPOINT_GROUPS = 'api/endpoint_groups';
|
||||
export const API_ENDPOINT_KUBERNETES = 'api/kubernetes';
|
||||
export const API_ENDPOINT_MOTD = 'api/motd';
|
||||
export const API_ENDPOINT_REGISTRIES = 'api/registries';
|
||||
export const API_ENDPOINT_RESOURCE_CONTROLS = 'api/resource_controls';
|
||||
export const API_ENDPOINT_SETTINGS = 'api/settings';
|
||||
export const API_ENDPOINT_STACKS = 'api/stacks';
|
||||
export const API_ENDPOINT_SUPPORT = 'api/support';
|
||||
export const API_ENDPOINT_USERS = 'api/users';
|
||||
export const API_ENDPOINT_TAGS = 'api/tags';
|
||||
export const API_ENDPOINT_TEAMS = 'api/teams';
|
||||
export const API_ENDPOINT_TEAM_MEMBERSHIPS = 'api/team_memberships';
|
||||
export const API_ENDPOINT_TEMPLATES = 'api/templates';
|
||||
export const API_ENDPOINT_WEBHOOKS = 'api/webhooks';
|
||||
export const PAGINATION_MAX_ITEMS = 10;
|
||||
export const APPLICATION_CACHE_VALIDITY = 3600;
|
||||
export const CONSOLE_COMMANDS_LABEL_PREFIX = 'io.portainer.commands.';
|
||||
export const PREDEFINED_NETWORKS = ['host', 'bridge', 'ingress', 'nat', 'none'];
|
||||
export const KUBERNETES_DEFAULT_NAMESPACE = 'default';
|
||||
export const KUBERNETES_SYSTEM_NAMESPACES = ['kube-system', 'kube-public', 'kube-node-lease', 'portainer'];
|
||||
export const PORTAINER_FADEOUT = 1500;
|
||||
export const STACK_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
|
||||
export const TEMPLATE_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
|
||||
|
||||
// don't declare new constants, either:
|
||||
// - if only used in one file or module, declare in that file or module (as a regular js constant)
|
||||
// - if needed across modules, declare like the above constant and use es6 import for that
|
||||
angular
|
||||
.module('portainer')
|
||||
.constant('API_ENDPOINT_AUTH', API_ENDPOINT_AUTH)
|
||||
.constant('API_ENDPOINT_BACKUP', API_ENDPOINT_BACKUP)
|
||||
.constant('API_ENDPOINT_CUSTOM_TEMPLATES', API_ENDPOINT_CUSTOM_TEMPLATES)
|
||||
.constant('API_ENDPOINT_EDGE_GROUPS', API_ENDPOINT_EDGE_GROUPS)
|
||||
.constant('API_ENDPOINT_EDGE_JOBS', API_ENDPOINT_EDGE_JOBS)
|
||||
.constant('API_ENDPOINT_EDGE_STACKS', API_ENDPOINT_EDGE_STACKS)
|
||||
.constant('API_ENDPOINT_EDGE_TEMPLATES', API_ENDPOINT_EDGE_TEMPLATES)
|
||||
.constant('API_ENDPOINT_ENDPOINTS', API_ENDPOINT_ENDPOINTS)
|
||||
.constant('API_ENDPOINT_ENDPOINT_GROUPS', API_ENDPOINT_ENDPOINT_GROUPS)
|
||||
.constant('API_ENDPOINT_KUBERNETES', API_ENDPOINT_KUBERNETES)
|
||||
.constant('API_ENDPOINT_MOTD', API_ENDPOINT_MOTD)
|
||||
.constant('API_ENDPOINT_REGISTRIES', API_ENDPOINT_REGISTRIES)
|
||||
.constant('API_ENDPOINT_RESOURCE_CONTROLS', API_ENDPOINT_RESOURCE_CONTROLS)
|
||||
.constant('API_ENDPOINT_SETTINGS', API_ENDPOINT_SETTINGS)
|
||||
.constant('API_ENDPOINT_STACKS', API_ENDPOINT_STACKS)
|
||||
.constant('API_ENDPOINT_SUPPORT', API_ENDPOINT_SUPPORT)
|
||||
.constant('API_ENDPOINT_USERS', API_ENDPOINT_USERS)
|
||||
.constant('API_ENDPOINT_TAGS', API_ENDPOINT_TAGS)
|
||||
.constant('API_ENDPOINT_TEAMS', API_ENDPOINT_TEAMS)
|
||||
.constant('API_ENDPOINT_TEAM_MEMBERSHIPS', API_ENDPOINT_TEAM_MEMBERSHIPS)
|
||||
.constant('API_ENDPOINT_TEMPLATES', API_ENDPOINT_TEMPLATES)
|
||||
.constant('API_ENDPOINT_WEBHOOKS', API_ENDPOINT_WEBHOOKS)
|
||||
.constant('PAGINATION_MAX_ITEMS', PAGINATION_MAX_ITEMS)
|
||||
.constant('APPLICATION_CACHE_VALIDITY', APPLICATION_CACHE_VALIDITY)
|
||||
.constant('CONSOLE_COMMANDS_LABEL_PREFIX', CONSOLE_COMMANDS_LABEL_PREFIX)
|
||||
.constant('PREDEFINED_NETWORKS', PREDEFINED_NETWORKS)
|
||||
.constant('BROWSER_OS_PLATFORM', BROWSER_OS_PLATFORM);
|
@ -0,0 +1,30 @@
|
||||
// try to declare constant where it's needed, not in a global file
|
||||
export const API_ENDPOINT_AUTH = 'api/auth';
|
||||
export const API_ENDPOINT_BACKUP = 'api/backup';
|
||||
export const API_ENDPOINT_CUSTOM_TEMPLATES = 'api/custom_templates';
|
||||
export const API_ENDPOINT_EDGE_GROUPS = 'api/edge_groups';
|
||||
export const API_ENDPOINT_EDGE_JOBS = 'api/edge_jobs';
|
||||
export const API_ENDPOINT_EDGE_STACKS = 'api/edge_stacks';
|
||||
export const API_ENDPOINT_EDGE_TEMPLATES = 'api/edge_templates';
|
||||
export const API_ENDPOINT_ENDPOINTS = 'api/endpoints';
|
||||
export const API_ENDPOINT_ENDPOINT_GROUPS = 'api/endpoint_groups';
|
||||
export const API_ENDPOINT_KUBERNETES = 'api/kubernetes';
|
||||
export const API_ENDPOINT_MOTD = 'api/motd';
|
||||
export const API_ENDPOINT_REGISTRIES = 'api/registries';
|
||||
export const API_ENDPOINT_RESOURCE_CONTROLS = 'api/resource_controls';
|
||||
export const API_ENDPOINT_SETTINGS = 'api/settings';
|
||||
export const API_ENDPOINT_STACKS = 'api/stacks';
|
||||
export const API_ENDPOINT_SUPPORT = 'api/support';
|
||||
export const API_ENDPOINT_USERS = 'api/users';
|
||||
export const API_ENDPOINT_TAGS = 'api/tags';
|
||||
export const API_ENDPOINT_TEAMS = 'api/teams';
|
||||
export const API_ENDPOINT_TEAM_MEMBERSHIPS = 'api/team_memberships';
|
||||
export const API_ENDPOINT_TEMPLATES = 'api/templates';
|
||||
export const API_ENDPOINT_WEBHOOKS = 'api/webhooks';
|
||||
export const PAGINATION_MAX_ITEMS = 10;
|
||||
export const APPLICATION_CACHE_VALIDITY = 3600;
|
||||
export const CONSOLE_COMMANDS_LABEL_PREFIX = 'io.portainer.commands.';
|
||||
export const PREDEFINED_NETWORKS = ['host', 'bridge', 'ingress', 'nat', 'none'];
|
||||
export const PORTAINER_FADEOUT = 1500;
|
||||
export const STACK_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
|
||||
export const TEMPLATE_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
|
@ -0,0 +1,65 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import {
|
||||
API_ENDPOINT_AUTH,
|
||||
API_ENDPOINT_BACKUP,
|
||||
API_ENDPOINT_CUSTOM_TEMPLATES,
|
||||
API_ENDPOINT_EDGE_GROUPS,
|
||||
API_ENDPOINT_EDGE_JOBS,
|
||||
API_ENDPOINT_EDGE_STACKS,
|
||||
API_ENDPOINT_EDGE_TEMPLATES,
|
||||
API_ENDPOINT_ENDPOINTS,
|
||||
API_ENDPOINT_ENDPOINT_GROUPS,
|
||||
API_ENDPOINT_KUBERNETES,
|
||||
API_ENDPOINT_MOTD,
|
||||
API_ENDPOINT_REGISTRIES,
|
||||
API_ENDPOINT_RESOURCE_CONTROLS,
|
||||
API_ENDPOINT_SETTINGS,
|
||||
API_ENDPOINT_STACKS,
|
||||
API_ENDPOINT_SUPPORT,
|
||||
API_ENDPOINT_USERS,
|
||||
API_ENDPOINT_TAGS,
|
||||
API_ENDPOINT_TEAMS,
|
||||
API_ENDPOINT_TEAM_MEMBERSHIPS,
|
||||
API_ENDPOINT_TEMPLATES,
|
||||
API_ENDPOINT_WEBHOOKS,
|
||||
PAGINATION_MAX_ITEMS,
|
||||
APPLICATION_CACHE_VALIDITY,
|
||||
CONSOLE_COMMANDS_LABEL_PREFIX,
|
||||
PREDEFINED_NETWORKS,
|
||||
} from './constants';
|
||||
import { BROWSER_OS_PLATFORM } from './react/constants';
|
||||
|
||||
// don't declare new constants, either:
|
||||
// - if only used in one file or module, declare in that file or module (as a regular js constant)
|
||||
// - if needed across modules, declare like in `./constants` and use es6 import for that
|
||||
|
||||
export const constantsModule = angular
|
||||
.module('portainer.app.constants', [])
|
||||
.constant('API_ENDPOINT_AUTH', API_ENDPOINT_AUTH)
|
||||
.constant('API_ENDPOINT_BACKUP', API_ENDPOINT_BACKUP)
|
||||
.constant('API_ENDPOINT_CUSTOM_TEMPLATES', API_ENDPOINT_CUSTOM_TEMPLATES)
|
||||
.constant('API_ENDPOINT_EDGE_GROUPS', API_ENDPOINT_EDGE_GROUPS)
|
||||
.constant('API_ENDPOINT_EDGE_JOBS', API_ENDPOINT_EDGE_JOBS)
|
||||
.constant('API_ENDPOINT_EDGE_STACKS', API_ENDPOINT_EDGE_STACKS)
|
||||
.constant('API_ENDPOINT_EDGE_TEMPLATES', API_ENDPOINT_EDGE_TEMPLATES)
|
||||
.constant('API_ENDPOINT_ENDPOINTS', API_ENDPOINT_ENDPOINTS)
|
||||
.constant('API_ENDPOINT_ENDPOINT_GROUPS', API_ENDPOINT_ENDPOINT_GROUPS)
|
||||
.constant('API_ENDPOINT_KUBERNETES', API_ENDPOINT_KUBERNETES)
|
||||
.constant('API_ENDPOINT_MOTD', API_ENDPOINT_MOTD)
|
||||
.constant('API_ENDPOINT_REGISTRIES', API_ENDPOINT_REGISTRIES)
|
||||
.constant('API_ENDPOINT_RESOURCE_CONTROLS', API_ENDPOINT_RESOURCE_CONTROLS)
|
||||
.constant('API_ENDPOINT_SETTINGS', API_ENDPOINT_SETTINGS)
|
||||
.constant('API_ENDPOINT_STACKS', API_ENDPOINT_STACKS)
|
||||
.constant('API_ENDPOINT_SUPPORT', API_ENDPOINT_SUPPORT)
|
||||
.constant('API_ENDPOINT_USERS', API_ENDPOINT_USERS)
|
||||
.constant('API_ENDPOINT_TAGS', API_ENDPOINT_TAGS)
|
||||
.constant('API_ENDPOINT_TEAMS', API_ENDPOINT_TEAMS)
|
||||
.constant('API_ENDPOINT_TEAM_MEMBERSHIPS', API_ENDPOINT_TEAM_MEMBERSHIPS)
|
||||
.constant('API_ENDPOINT_TEMPLATES', API_ENDPOINT_TEMPLATES)
|
||||
.constant('API_ENDPOINT_WEBHOOKS', API_ENDPOINT_WEBHOOKS)
|
||||
.constant('PAGINATION_MAX_ITEMS', PAGINATION_MAX_ITEMS)
|
||||
.constant('APPLICATION_CACHE_VALIDITY', APPLICATION_CACHE_VALIDITY)
|
||||
.constant('CONSOLE_COMMANDS_LABEL_PREFIX', CONSOLE_COMMANDS_LABEL_PREFIX)
|
||||
.constant('PREDEFINED_NETWORKS', PREDEFINED_NETWORKS)
|
||||
.constant('BROWSER_OS_PLATFORM', BROWSER_OS_PLATFORM).name;
|
@ -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;
|
@ -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>
|
@ -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: '<',
|
||||
},
|
||||
};
|
@ -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;
|
@ -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>
|
@ -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: '<',
|
||||
},
|
||||
};
|
@ -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);
|
||||
}
|
||||
}
|
@ -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: '<',
|
||||
},
|
||||
};
|
@ -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;
|
@ -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);
|
||||
}
|
@ -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>
|
@ -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: '<',
|
||||
},
|
||||
};
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
@ -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;
|
@ -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 <a href=\'https://docs.portainer.io/user/kubernetes/applications/webhooks\' target=\'_blank\' rel=\'noreferrer\'>Portainer documentation on webhook usage</a>.' :
|
||||
'See <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>
|
@ -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: '<',
|
||||
},
|
||||
};
|
@ -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>
|
@ -1,9 +0,0 @@
|
||||
export const gitFormComposePathField = {
|
||||
templateUrl: './git-form-compose-path-field.html',
|
||||
bindings: {
|
||||
deployMethod: '@',
|
||||
value: '<',
|
||||
onChange: '<',
|
||||
isDockerStandalone: '<',
|
||||
},
|
||||
};
|
@ -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>
|
@ -1,10 +0,0 @@
|
||||
export const gitFormInfoPanel = {
|
||||
templateUrl: './git-form-info-panel.html',
|
||||
bindings: {
|
||||
url: '<',
|
||||
configFilePath: '<',
|
||||
additionalFiles: '<',
|
||||
className: '@',
|
||||
type: '@',
|
||||
},
|
||||
};
|
@ -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: '<',
|
||||
},
|
||||
};
|
@ -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>
|
@ -1,7 +0,0 @@
|
||||
export const gitFormRefField = {
|
||||
templateUrl: './git-form-ref-field.html',
|
||||
bindings: {
|
||||
value: '<',
|
||||
onChange: '<',
|
||||
},
|
||||
};
|
@ -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>
|
@ -1,7 +0,0 @@
|
||||
export const gitFormUrlField = {
|
||||
templateUrl: './git-form-url-field.html',
|
||||
bindings: {
|
||||
value: '<',
|
||||
onChange: '<',
|
||||
},
|
||||
};
|
@ -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';
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
@ -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: '@',
|
||||
},
|
||||
};
|
@ -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,
|
||||
};
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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>
|
@ -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);
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { WebhookHelperFactory } from './webhookHelper';
|
||||
|
||||
export const helpersModule = angular
|
||||
.module('portainer.app.helpers', [])
|
||||
.factory('WebhookHelper', WebhookHelperFactory).name;
|
@ -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;
|
||||
},
|
||||
]);
|
@ -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()}`;
|
||||
}
|
@ -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;
|
@ -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`;
|
||||
}
|
@ -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;
|
||||
};
|
@ -0,0 +1,87 @@
|
||||
import moment from 'moment';
|
||||
import 'moment-timezone';
|
||||
|
||||
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
|
||||
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
|
||||
import { withEdition } from '../portainer/feature-flags/withEdition';
|
||||
|
||||
const TimeWindowDisplayWrapper = withEdition(TimeWindowDisplay, 'BE');
|
||||
|
||||
export { TimeWindowDisplayWrapper as TimeWindowDisplay };
|
||||
|
||||
function TimeWindowDisplay() {
|
||||
const currentEnvQuery = useCurrentEnvironment(false);
|
||||
|
||||
if (!currentEnvQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { ChangeWindow } = currentEnvQuery.data;
|
||||
|
||||
if (!ChangeWindow.Enabled) {
|
||||
return null;
|
||||
}
|
||||
const timezone = moment.tz.guess();
|
||||
const isDST = moment().isDST();
|
||||
const { startTime: startTimeLocal, endTime: endTimeLocal } = utcToTime(
|
||||
{ startTime: ChangeWindow.StartTime, endTime: ChangeWindow.EndTime },
|
||||
timezone
|
||||
);
|
||||
|
||||
const { startTime: startTimeUtc, endTime: endTimeUtc } = parseInterval(
|
||||
ChangeWindow.StartTime,
|
||||
ChangeWindow.EndTime
|
||||
);
|
||||
|
||||
return (
|
||||
<TextTip color="orange">
|
||||
A change window is enabled, automatic updates will not occur outside of{' '}
|
||||
<span className="font-bold">
|
||||
{shortTime(startTimeUtc)} - {shortTime(endTimeUtc)} UTC (
|
||||
{shortTime(startTimeLocal)} -{shortTime(endTimeLocal)}{' '}
|
||||
{isDST ? 'DST' : ''} {timezone})
|
||||
</span>
|
||||
.
|
||||
</TextTip>
|
||||
);
|
||||
}
|
||||
|
||||
function utcToTime(
|
||||
utcTime: { startTime: string; endTime: string },
|
||||
timezone: string
|
||||
) {
|
||||
const startTime = moment
|
||||
.tz(utcTime.startTime, 'HH:mm', 'GMT')
|
||||
.tz(timezone)
|
||||
.format('HH:mm');
|
||||
const endTime = moment
|
||||
.tz(utcTime.endTime, 'HH:mm', 'GMT')
|
||||
.tz(timezone)
|
||||
.format('HH:mm');
|
||||
|
||||
return parseInterval(startTime, endTime);
|
||||
}
|
||||
|
||||
function parseTime(originalTime: string) {
|
||||
const [startHour, startMin] = originalTime.split(':');
|
||||
|
||||
const time = new Date();
|
||||
|
||||
time.setHours(parseInt(startHour, 10));
|
||||
time.setMinutes(parseInt(startMin, 10));
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
function parseInterval(startTime: string, endTime: string) {
|
||||
return {
|
||||
startTime: parseTime(startTime),
|
||||
endTime: parseTime(endTime),
|
||||
};
|
||||
}
|
||||
|
||||
function shortTime(time: Date) {
|
||||
return moment(time).format('h:mm a');
|
||||
}
|
@ -1 +1 @@
|
||||
export { InputList } from './InputList';
|
||||
export { InputList, type ItemProps } from './InputList';
|
||||
|
@ -0,0 +1,27 @@
|
||||
import { useRef } from 'react';
|
||||
import { TestContext, TestFunction } from 'yup';
|
||||
|
||||
function cacheTest<T, TContext>(
|
||||
asyncValidate: TestFunction<T, TContext>
|
||||
): TestFunction<T, TContext> {
|
||||
let valid = true;
|
||||
let value: T | undefined;
|
||||
|
||||
return async (newValue: T, context: TestContext<TContext>) => {
|
||||
if (newValue !== value) {
|
||||
value = newValue;
|
||||
|
||||
const response = await asyncValidate.call(context, newValue, context);
|
||||
valid = !!response;
|
||||
}
|
||||
return valid;
|
||||
};
|
||||
}
|
||||
|
||||
export function useCachedValidation<T, TContext>(
|
||||
test: TestFunction<T, TContext>
|
||||
) {
|
||||
const ref = useRef(cacheTest(test));
|
||||
|
||||
return ref.current;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { useRef, useState, useCallback, useEffect } from 'react';
|
||||
|
||||
export function useCaretPosition<
|
||||
T extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement
|
||||
>() {
|
||||
const node = useRef<T>(null);
|
||||
const [start, setStart] = useState(0);
|
||||
const [end, setEnd] = useState(0);
|
||||
|
||||
const updateCaret = useCallback(() => {
|
||||
if (node.current) {
|
||||
const { selectionStart, selectionEnd } = node.current;
|
||||
|
||||
setStart(selectionStart || 0);
|
||||
setEnd(selectionEnd || 0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (node.current) {
|
||||
node.current.setSelectionRange(start, end);
|
||||
}
|
||||
});
|
||||
|
||||
return { start, end, ref: node, updateCaret };
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import { yupToFormErrors } from 'formik';
|
||||
import { SchemaOf } from 'yup';
|
||||
|
||||
export async function validateForm<T>(
|
||||
schemaBuilder: () => SchemaOf<T>,
|
||||
formValues: T
|
||||
) {
|
||||
const validationSchema = schemaBuilder();
|
||||
|
||||
try {
|
||||
await validationSchema.validate(formValues, {
|
||||
strict: true,
|
||||
abortEarly: false,
|
||||
});
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
return yupToFormErrors<T>(error);
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { useEnvironment } from '@/react/portainer/environments/queries/useEnvironment';
|
||||
import { useEnvironment } from '@/react/portainer/environments/queries';
|
||||
|
||||
import { useEnvironmentId } from './useEnvironmentId';
|
||||
|
||||
export function useCurrentEnvironment() {
|
||||
const id = useEnvironmentId();
|
||||
export function useCurrentEnvironment(force = true) {
|
||||
const id = useEnvironmentId(force);
|
||||
return useEnvironment(id);
|
||||
}
|
||||
|
@ -1,21 +1,22 @@
|
||||
import _ from 'lodash';
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
|
||||
export function useDebounce(
|
||||
defaultValue: string,
|
||||
onChange: (value: string) => void
|
||||
) {
|
||||
const [searchValue, setSearchValue] = useState(defaultValue);
|
||||
export function useDebounce(value: string, onChange: (value: string) => void) {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
const onChangeDebounces = useRef(_.debounce(onChange, 300));
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value: string) => {
|
||||
setSearchValue(value);
|
||||
setDebouncedValue(value);
|
||||
onChangeDebounces.current(value);
|
||||
},
|
||||
[onChangeDebounces, setSearchValue]
|
||||
[onChangeDebounces, setDebouncedValue]
|
||||
);
|
||||
|
||||
return [searchValue, handleChange] as const;
|
||||
useEffect(() => {
|
||||
setDebouncedValue(value);
|
||||
}, [value]);
|
||||
|
||||
return [debouncedValue, handleChange] as const;
|
||||
}
|
||||
|
@ -1,13 +1,19 @@
|
||||
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||
|
||||
export function useEnvironmentId() {
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
export function useEnvironmentId(force = true): EnvironmentId {
|
||||
const {
|
||||
params: { endpointId: environmentId },
|
||||
} = useCurrentStateAndParams();
|
||||
|
||||
if (!environmentId) {
|
||||
if (!force) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
throw new Error('endpointId url param is required');
|
||||
}
|
||||
|
||||
return environmentId;
|
||||
return parseInt(environmentId, 10);
|
||||
}
|
||||
|
@ -0,0 +1,56 @@
|
||||
import { FormikErrors } from 'formik';
|
||||
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
import { InputGroup } from '@@/form-components/InputGroup';
|
||||
import { InputList, ItemProps } from '@@/form-components/InputList';
|
||||
import { useCaretPosition } from '@@/form-components/useCaretPosition';
|
||||
|
||||
interface Props {
|
||||
value: Array<string>;
|
||||
onChange: (value: Array<string>) => void;
|
||||
errors?: FormikErrors<string>[] | string | string[];
|
||||
}
|
||||
|
||||
export function AdditionalFileField({ onChange, value, errors }: Props) {
|
||||
return (
|
||||
<InputList
|
||||
errors={errors}
|
||||
label="Additional paths"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
addLabel="Add file"
|
||||
item={Item}
|
||||
itemBuilder={() => ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Item({
|
||||
item,
|
||||
onChange,
|
||||
disabled,
|
||||
error,
|
||||
readOnly,
|
||||
}: ItemProps<string>) {
|
||||
const { ref, updateCaret } = useCaretPosition();
|
||||
|
||||
return (
|
||||
<>
|
||||
<InputGroup size="small" className="col-sm-5">
|
||||
<InputGroup.Addon>path</InputGroup.Addon>
|
||||
<InputGroup.Input
|
||||
mRef={ref}
|
||||
required
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
value={item}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
updateCaret();
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
{error && <FormError>{error}</FormError>}
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
import { FormikErrors } from 'formik';
|
||||
import { boolean, number, object, SchemaOf, string } from 'yup';
|
||||
|
||||
import { GitAuthModel } from '@/react/portainer/gitops/types';
|
||||
import { useDebounce } from '@/react/hooks/useDebounce';
|
||||
import { GitCredential } from '@/portainer/views/account/git-credential/types';
|
||||
|
||||
import { SwitchField } from '@@/form-components/SwitchField';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
|
||||
import { isBE } from '../../feature-flags/feature-flags.service';
|
||||
|
||||
import { CredentialSelector } from './CredentialSelector';
|
||||
import { NewCredentialForm } from './NewCredentialForm';
|
||||
|
||||
interface Props {
|
||||
value: GitAuthModel;
|
||||
onChange: (value: Partial<GitAuthModel>) => void;
|
||||
isExplanationVisible?: boolean;
|
||||
errors?: FormikErrors<GitAuthModel>;
|
||||
}
|
||||
|
||||
export function AuthFieldset({
|
||||
value,
|
||||
onChange,
|
||||
isExplanationVisible,
|
||||
errors,
|
||||
}: Props) {
|
||||
const [username, setUsername] = useDebounce(
|
||||
value.RepositoryUsername || '',
|
||||
(username) => handleChange({ RepositoryUsername: username })
|
||||
);
|
||||
const [password, setPassword] = useDebounce(
|
||||
value.RepositoryPassword || '',
|
||||
(password) => handleChange({ RepositoryPassword: password })
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<SwitchField
|
||||
label="Authentication"
|
||||
labelClass="col-sm-3 col-lg-2"
|
||||
name="authentication"
|
||||
checked={value.RepositoryAuthentication}
|
||||
onChange={(value) =>
|
||||
handleChange({ RepositoryAuthentication: value })
|
||||
}
|
||||
data-cy="component-gitAuthToggle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{value.RepositoryAuthentication && (
|
||||
<>
|
||||
{isExplanationVisible && (
|
||||
<TextTip color="orange">
|
||||
Enabling authentication will store the credentials and it is
|
||||
advisable to use a git service account
|
||||
</TextTip>
|
||||
)}
|
||||
|
||||
{isBE && (
|
||||
<CredentialSelector
|
||||
onChange={handleChangeGitCredential}
|
||||
value={value.RepositoryGitCredentialID}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<FormControl label="Username" errors={errors?.RepositoryUsername}>
|
||||
<Input
|
||||
value={username}
|
||||
name="repository_username"
|
||||
placeholder={
|
||||
value.RepositoryGitCredentialID ? '' : 'git username'
|
||||
}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
data-cy="component-gitUsernameInput"
|
||||
readOnly={!!value.RepositoryGitCredentialID}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group !mb-0">
|
||||
<div className="col-sm-12">
|
||||
<FormControl
|
||||
label="Personal Access Token"
|
||||
tooltip="Provide a personal access token or password"
|
||||
errors={errors?.RepositoryPassword}
|
||||
>
|
||||
<Input
|
||||
type="password"
|
||||
value={password}
|
||||
name="repository_password"
|
||||
placeholder="*******"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
data-cy="component-gitPasswordInput"
|
||||
readOnly={!!value.RepositoryGitCredentialID}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
{!value.RepositoryGitCredentialID &&
|
||||
value.RepositoryPassword &&
|
||||
isBE && (
|
||||
<NewCredentialForm
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
errors={errors}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
function handleChangeGitCredential(gitCredential?: GitCredential | null) {
|
||||
handleChange(
|
||||
gitCredential
|
||||
? {
|
||||
RepositoryGitCredentialID: gitCredential.id,
|
||||
RepositoryUsername: gitCredential?.username,
|
||||
RepositoryPassword: '',
|
||||
SaveCredential: false,
|
||||
NewCredentialName: '',
|
||||
}
|
||||
: {
|
||||
RepositoryGitCredentialID: 0,
|
||||
RepositoryUsername: '',
|
||||
RepositoryPassword: '',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function handleChange(partialValue: Partial<GitAuthModel>) {
|
||||
onChange(partialValue);
|
||||
}
|
||||
}
|
||||
|
||||
export function gitAuthValidation(
|
||||
gitCredentials: Array<GitCredential>
|
||||
): SchemaOf<GitAuthModel> {
|
||||
return object({
|
||||
RepositoryAuthentication: boolean().default(false),
|
||||
RepositoryGitCredentialID: number().default(0),
|
||||
RepositoryUsername: string()
|
||||
.when(['RepositoryAuthentication', 'RepositoryGitCredentialID'], {
|
||||
is: (auth: boolean, id: number) => auth && !id,
|
||||
then: string().required('Username is required'),
|
||||
})
|
||||
.default(''),
|
||||
RepositoryPassword: string()
|
||||
.when(['RepositoryAuthentication', 'RepositoryGitCredentialID'], {
|
||||
is: (auth: boolean, id: number) => auth && !id,
|
||||
then: string().required('Password is required'),
|
||||
})
|
||||
.default(''),
|
||||
SaveCredential: boolean().default(false),
|
||||
NewCredentialName: string()
|
||||
.default('')
|
||||
.when(['RepositoryAuthentication', 'SaveCredential'], {
|
||||
is: true,
|
||||
then: string()
|
||||
.required('Name is required')
|
||||
.test(
|
||||
'is-unique',
|
||||
'This name is already been used, please try another one',
|
||||
(name) => !!name && !gitCredentials.find((x) => x.name === name)
|
||||
)
|
||||
.matches(
|
||||
/^[-_a-z0-9]+$/,
|
||||
"This field must consist of lower case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123')."
|
||||
),
|
||||
}),
|
||||
});
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
import { useGitCredentials } from '@/portainer/views/account/git-credential/gitCredential.service';
|
||||
import { GitCredential } from '@/portainer/views/account/git-credential/types';
|
||||
import { useUser } from '@/react/hooks/useUser';
|
||||
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Select } from '@@/form-components/ReactSelect';
|
||||
|
||||
export function CredentialSelector({
|
||||
value,
|
||||
onChange,
|
||||
error,
|
||||
}: {
|
||||
value?: number;
|
||||
onChange(gitCredential?: GitCredential | null): void;
|
||||
error?: string;
|
||||
}) {
|
||||
const { user } = useUser();
|
||||
|
||||
const gitCredentialsQuery = useGitCredentials(user.Id);
|
||||
|
||||
const gitCredentials = gitCredentialsQuery.data ?? [];
|
||||
|
||||
return (
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<FormControl
|
||||
label="Git Credentials"
|
||||
inputId="git-creds-selector"
|
||||
errors={error}
|
||||
>
|
||||
<Select
|
||||
placeholder="select git credential or fill in below"
|
||||
value={gitCredentials.find(
|
||||
(gitCredential) => gitCredential.id === value
|
||||
)}
|
||||
options={gitCredentials}
|
||||
getOptionLabel={(gitCredential) => gitCredential.name}
|
||||
getOptionValue={(gitCredential) => gitCredential.id.toString()}
|
||||
onChange={onChange}
|
||||
isClearable
|
||||
noOptionsMessage={() => 'no saved credentials'}
|
||||
inputId="git-creds-selector"
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import { FormikErrors } from 'formik';
|
||||
|
||||
import { Checkbox } from '@@/form-components/Checkbox';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
|
||||
import { GitAuthModel } from '../types';
|
||||
|
||||
export function NewCredentialForm({
|
||||
value,
|
||||
onChange,
|
||||
errors,
|
||||
}: {
|
||||
value: GitAuthModel;
|
||||
onChange: (value: Partial<GitAuthModel>) => void;
|
||||
errors?: FormikErrors<GitAuthModel>;
|
||||
}) {
|
||||
return (
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<FormControl label="">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="repository-save-credential"
|
||||
label="save credential"
|
||||
checked={value.SaveCredential}
|
||||
className="[&+label]:mb-0"
|
||||
onChange={(e) => onChange({ SaveCredential: e.target.checked })}
|
||||
/>
|
||||
<Input
|
||||
value={value.NewCredentialName}
|
||||
name="new_credential_name"
|
||||
placeholder="credential name"
|
||||
className="ml-4 w-48"
|
||||
onChange={(e) => onChange({ NewCredentialName: e.target.value })}
|
||||
disabled={!value.SaveCredential}
|
||||
/>
|
||||
{errors?.NewCredentialName && (
|
||||
<div className="small text-danger">
|
||||
{errors.NewCredentialName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{value.SaveCredential && (
|
||||
<TextTip color="blue" className="!mb-0">
|
||||
This git credential can be managed through your account page
|
||||
</TextTip>
|
||||
)}
|
||||
</div>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue