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

feat(templates): allow managing git based templates [EE-2600] (#7855)

Co-authored-by: itsconquest <william.conquest@portainer.io>
Co-authored-by: oscarzhou <oscar.zhou@portainer.io>
Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
This commit is contained in:
Oscar Zhou 2023-04-04 12:44:42 +12:00 committed by GitHub
parent 30a2bb0495
commit c650868fe9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 944 additions and 101 deletions

View file

@ -1,7 +1,7 @@
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { editor, upload } from '@@/BoxSelector/common-options/build-methods';
import { editor, upload, git } from '@@/BoxSelector/common-options/build-methods';
import { confirmWebEditorDiscard } from '@@/modals/confirm';
class KubeCreateCustomTemplateViewController {
@ -9,7 +9,7 @@ class KubeCreateCustomTemplateViewController {
constructor($async, $state, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService) {
Object.assign(this, { $async, $state, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService });
this.methodOptions = [editor, upload];
this.methodOptions = [editor, upload, git];
this.templates = null;
this.isTemplateVariablesEnabled = isBE;
@ -31,6 +31,13 @@ class KubeCreateCustomTemplateViewController {
Logo: '',
AccessControlData: new AccessControlFormData(),
Variables: [],
RepositoryURL: '',
RepositoryURLValid: false,
RepositoryReferenceName: 'refs/heads/main',
RepositoryAuthentication: false,
RepositoryUsername: '',
RepositoryPassword: '',
ComposeFilePathInRepository: 'manifest.yml',
};
this.onChangeFile = this.onChangeFile.bind(this);
@ -121,6 +128,8 @@ class KubeCreateCustomTemplateViewController {
return this.createCustomTemplateFromFileContent(template);
case 'upload':
return this.createCustomTemplateFromFileUpload(template);
case 'repository':
return this.createCustomTemplateFromGitRepository(template);
}
}
@ -132,6 +141,10 @@ class KubeCreateCustomTemplateViewController {
return this.CustomTemplateService.createCustomTemplateFromFileUpload(template);
}
createCustomTemplateFromGitRepository(template) {
return this.CustomTemplateService.createCustomTemplateFromGitRepository(template);
}
validateForm(method) {
this.state.formValidationError = '';

View file

@ -35,6 +35,8 @@
<file-upload-description> You can upload a Manifest file from your computer. </file-upload-description>
</file-upload-form>
<git-form deploy-method="kubernetes" ng-if="$ctrl.state.method === 'repository'" value="$ctrl.formValues" on-change="($ctrl.handleChange)"></git-form>
<custom-templates-variables-definition-field
ng-if="$ctrl.isTemplateVariablesEnabled"
value="$ctrl.formValues.Variables"

View file

@ -3,6 +3,7 @@ import { AccessControlFormData } from '@/portainer/components/accessControlForm/
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
import { confirmWebEditorDiscard } from '@@/modals/confirm';
import { getFilePreview } from '@/react/portainer/gitops/gitops.service';
class KubeEditCustomTemplateViewController {
/* @ngInject */
@ -11,11 +12,18 @@ class KubeEditCustomTemplateViewController {
this.isTemplateVariablesEnabled = isBE;
this.formValues = null;
this.formValues = {
Variables: [],
TLSSkipVerify: false,
};
this.state = {
formValidationError: '',
isEditorDirty: false,
isTemplateValid: true,
isEditorReadOnly: false,
templateLoadFailed: false,
templatePreviewFailed: false,
templatePreviewError: '',
};
this.templates = [];
@ -25,6 +33,7 @@ class KubeEditCustomTemplateViewController {
this.onBeforeUnload = this.onBeforeUnload.bind(this);
this.handleChange = this.handleChange.bind(this);
this.onVariablesChange = this.onVariablesChange.bind(this);
this.previewFileFromGitRepository = this.previewFileFromGitRepository.bind(this);
}
getTemplate() {
@ -32,13 +41,25 @@ class KubeEditCustomTemplateViewController {
try {
const { id } = this.$state.params;
const [template, file] = await Promise.all([this.CustomTemplateService.customTemplate(id), this.CustomTemplateService.customTemplateFile(id)]);
template.FileContent = file;
const template = await this.CustomTemplateService.customTemplate(id);
if (template.GitConfig !== null) {
this.state.isEditorReadOnly = true;
}
try {
template.FileContent = await this.CustomTemplateService.customTemplateFile(id, template.GitConfig !== null);
} catch (err) {
this.state.templateLoadFailed = true;
throw err;
}
template.Variables = template.Variables || [];
this.formValues = template;
this.formValues = { ...this.formValues, ...template };
this.parseTemplate(file);
this.parseTemplate(template.FileContent);
this.parseGitConfig(template.GitConfig);
this.oldFileContent = this.formValues.FileContent;
@ -79,6 +100,62 @@ class KubeEditCustomTemplateViewController {
}
}
parseGitConfig(config) {
if (config === null) {
return;
}
let flatConfig = {
RepositoryURL: config.URL,
RepositoryReferenceName: config.ReferenceName,
ComposeFilePathInRepository: config.ConfigFilePath,
RepositoryAuthentication: config.Authentication !== null,
TLSSkipVerify: config.TLSSkipVerify,
};
if (config.Authentication) {
flatConfig = {
...flatConfig,
RepositoryUsername: config.Authentication.Username,
RepositoryPassword: config.Authentication.Password,
};
}
this.formValues = { ...this.formValues, ...flatConfig };
}
previewFileFromGitRepository() {
this.state.templatePreviewFailed = false;
this.state.templatePreviewError = '';
let creds = {};
if (this.formValues.RepositoryAuthentication) {
creds = {
username: this.formValues.RepositoryUsername,
password: this.formValues.RepositoryPassword,
};
}
const payload = {
repository: this.formValues.RepositoryURL,
targetFile: this.formValues.ComposeFilePathInRepository,
tlsSkipVerify: this.formValues.TLSSkipVerify,
...creds,
};
this.$async(async () => {
try {
this.formValues.FileContent = await getFilePreview(payload);
this.state.isEditorDirty = true;
// check if the template contains mustache template symbol
this.parseTemplate(this.formValues.FileContent);
} catch (err) {
this.state.templatePreviewError = err.message;
this.state.templatePreviewFailed = true;
}
});
}
validateForm() {
this.state.formValidationError = '';

View file

@ -7,6 +7,22 @@
<form class="form-horizontal" name="$ctrl.form">
<custom-template-common-fields form-values="$ctrl.formValues"></custom-template-common-fields>
<git-form value="$ctrl.formValues" on-change="($ctrl.handleChange)" ng-if="$ctrl.formValues.GitConfig"></git-form>
<div class="form-group">
<div class="col-sm-12"
><button type="button" class="btn btn-sm btn-light !ml-0" ng-if="$ctrl.formValues.GitConfig" ng-click="$ctrl.previewFileFromGitRepository()">
<pr-icon icon="'refresh-cw'" feather="true"></pr-icon>Reload custom template</button
>
</div>
<div class="col-sm-12" ng-if="$ctrl.state.templatePreviewFailed">
<p class="small vertical-center text-danger mt-5">
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>
Custom template could not be loaded, {{ $ctrl.state.templatePreviewError }}.</p
>
</div>
</div>
<web-editor-form
identifier="template-editor"
value="$ctrl.formValues.FileContent"
@ -14,6 +30,7 @@
ng-required="true"
yml="true"
placeholder="Define or paste the content of your manifest file here"
read-only="$ctrl.state.isEditorReadOnly"
>
<editor-description>
<p>Templates allow deploying any kind of Kubernetes resource (Deployment, Secret, ConfigMap...)</p>
@ -31,7 +48,11 @@
is-variables-names-from-parent="true"
></custom-templates-variables-definition-field>
<por-access-control-form form-data="$ctrl.formValues.AccessControlData" resource-control="$ctrl.formValues.ResourceControl"></por-access-control-form>
<por-access-control-form
form-data="$ctrl.formValues.AccessControlData"
resource-control="$ctrl.formValues.ResourceControl"
ng-if="$ctrl.formValues.AccessControlData"
></por-access-control-form>
<div class="col-sm-12 form-section-title"> Actions </div>
<div class="form-group">

View file

@ -105,6 +105,16 @@
></custom-templates-variables-field>
</div>
<span ng-if="ctrl.state.BuildMethod === ctrl.BuildMethods.CUSTOM_TEMPLATE && ctrl.state.templateId && ctrl.state.templateLoadFailed">
<p class="small vertical-center text-danger mb-5" ng-if="ctrl.currentUser.isAdmin || ctrl.currentUser.id === ctrl.state.template.CreatedByUserId">
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>Custom template could not be loaded, please
<a ui-sref="kubernetes.templates.custom.edit({id: ctrl.state.templateId})">click here</a> for configuration.</p
>
<p class="small vertical-center text-danger mb-5" ng-if="!(ctrl.currentUser.isAdmin || ctrl.currentUser.id === ctrl.state.template.CreatedByUserId)">
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>Custom template could not be loaded, please contact your administrator.</p
>
</span>
<!-- editor -->
<div class="mt-4">
<web-editor-form
@ -115,6 +125,7 @@
ng-required="true"
yml="true"
placeholder="Define or paste the content of your manifest file here"
read-only="ctrl.state.isEditorReadOnly"
>
<editor-description>
<p class="vertical-center">

View file

@ -46,6 +46,13 @@ class KubernetesDeployController {
template: null,
baseWebhookUrl: baseStackWebhookUrl(),
webhookId: createWebhookId(),
templateLoadFailed: false,
isEditorReadOnly: false,
};
this.currentUser = {
isAdmin: false,
id: null,
};
this.formValues = {
@ -95,7 +102,7 @@ class KubernetesDeployController {
const metadata = {
type: buildLabel(this.state.BuildMethod),
format: formatLabel(this.state.DeployType),
role: roleLabel(this.Authentication.isAdmin()),
role: roleLabel(this.currentUser.isAdmin),
'automatic-updates': automaticUpdatesLabel(this.formValues.RepositoryAutomaticUpdates, this.formValues.RepositoryMechanism),
};
@ -183,9 +190,15 @@ class KubernetesDeployController {
this.state.template = template;
try {
const fileContent = await this.CustomTemplateService.customTemplateFile(templateId);
this.state.templateContent = fileContent;
this.onChangeFileContent(fileContent);
try {
this.state.templateContent = await this.CustomTemplateService.customTemplateFile(templateId, template.GitConfig !== null);
this.onChangeFileContent(this.state.templateContent);
this.state.isEditorReadOnly = true;
} catch (err) {
this.state.templateLoadFailed = true;
throw err;
}
if (template.Variables && template.Variables.length > 0) {
const variables = Object.fromEntries(template.Variables.map((variable) => [variable.name, '']));
@ -318,6 +331,9 @@ class KubernetesDeployController {
$onInit() {
return this.$async(async () => {
this.currentUser.isAdmin = this.Authentication.isAdmin();
this.currentUser.id = this.Authentication.getUserDetails().ID;
this.formValues.namespace_toggle = false;
await this.getNamespaces();

View file

@ -1,8 +1,15 @@
angular.module('portainer.app').controller('CodeEditorController', function CodeEditorController($document, CodeMirrorService, $scope) {
var ctrl = this;
this.$onChanges = function $onChanges({ value }) {
if (value && value.currentValue && ctrl.editor && ctrl.editor.getValue() !== value.currentValue) {
this.$onChanges = function $onChanges({ value, readOnly }) {
if (!ctrl.editor) {
return;
}
if (readOnly && typeof readOnly.currentValue === 'boolean' && ctrl.editor.getValue('readOnly') !== ctrl.readOnly) {
ctrl.editor.setOption('readOnly', ctrl.readOnly);
}
if (value && value.currentValue && ctrl.editor.getValue() !== value.currentValue) {
ctrl.editor.setValue(value.currentValue);
}

View file

@ -13,6 +13,7 @@ function CustomTemplatesFactory($resource, API_ENDPOINT_CUSTOM_TEMPLATES) {
update: { method: 'PUT', params: { id: '@id' } },
remove: { method: 'DELETE', params: { id: '@id' } },
file: { method: 'GET', params: { id: '@id', action: 'file' } },
gitFetch: { method: 'PUT', params: { id: '@id', action: 'git_fetch' } },
}
);
}

View file

@ -1,4 +1,5 @@
import angular from 'angular';
import PortainerError from 'Portainer/error';
angular.module('portainer.app').factory('CustomTemplateService', CustomTemplateServiceFactory);
@ -24,12 +25,12 @@ function CustomTemplateServiceFactory($sanitize, CustomTemplates, FileUploadServ
return CustomTemplates.remove({ id }).$promise;
};
service.customTemplateFile = async function customTemplateFile(id) {
service.customTemplateFile = async function customTemplateFile(id, remote = false) {
try {
const { FileContent } = await CustomTemplates.file({ id }).$promise;
const { FileContent } = remote ? await CustomTemplates.gitFetch({ id }).$promise : await CustomTemplates.file({ id }).$promise;
return FileContent;
} catch (err) {
throw { msg: 'Unable to retrieve customTemplate content', err };
throw new PortainerError('Unable to retrieve custom template content', err);
}
};

View file

@ -18,16 +18,27 @@
on-change="($ctrl.onChangeTemplateVariables)"
></custom-templates-variables-field>
<div class="form-group">
<div class="form-group" ng-if="$ctrl.state.selectedTemplate && !$ctrl.state.templateLoadFailed">
<div class="col-sm-12">
<a class="small interactive vertical-center" ng-show="!$ctrl.state.showAdvancedOptions" ng-click="$ctrl.state.showAdvancedOptions = true;">
<pr-icon icon="'plus'" class-name="'space-right'"></pr-icon> Customize stack
<pr-icon icon="'plus'" class-name="space-right" feather="true"></pr-icon> {{ $ctrl.state.selectedTemplate.GitConfig !== null ? 'View' : 'Customize' }} stack
</a>
<a class="small interactive vertical-center" ng-show="$ctrl.state.showAdvancedOptions" ng-click="$ctrl.state.showAdvancedOptions = false;">
<pr-icon icon="'minus'" class-name="'space-right'"></pr-icon> Hide custom stack
<pr-icon icon="'minus'" class-name="space-right" feather="true"></pr-icon> Hide {{ $ctrl.state.selectedTemplate.GitConfig === null ? 'custom' : '' }} stack
</a>
</div>
</div>
<span ng-if="$ctrl.state.selectedTemplate && $ctrl.state.templateLoadFailed">
<p class="small vertical-center text-danger mb-5" ng-if="$ctrl.currentUser.isAdmin || $ctrl.currentUser.id === $ctrl.state.selectedTemplate.CreatedByUserId">
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>Custom template could not be loaded, please
<a ui-sref="docker.templates.custom.edit({id: $ctrl.state.selectedTemplate.Id})">click here</a> for configuration.</p
>
<p class="small vertical-center text-danger mb-5" ng-if="!($ctrl.currentUser.isAdmin || $ctrl.currentUser.id === $ctrl.state.selectedTemplate.CreatedByUserId)">
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>Custom template could not be loaded, please contact your administrator.</p
>
</span>
<!-- web-editor -->
<web-editor-form
ng-if="$ctrl.state.showAdvancedOptions"
@ -37,6 +48,7 @@
ng-required="true"
yml="true"
placeholder="Define or paste the content of your docker compose file here"
read-only="$ctrl.state.isEditorReadOnly"
>
<editor-description>
<p>

View file

@ -44,10 +44,10 @@ class CustomTemplatesViewController {
showAdvancedOptions: false,
formValidationError: '',
actionInProgress: false,
isEditorVisible: false,
deployable: false,
templateNameRegex: TEMPLATE_NAME_VALIDATION_REGEX,
templateContent: '',
templateLoadFailed: false,
};
this.currentUser = {
@ -204,6 +204,13 @@ class CustomTemplatesViewController {
template.Selected = true;
try {
this.state.templateContent = this.formValues.fileContent = await this.CustomTemplateService.customTemplateFile(template.Id, template.GitConfig !== null);
} catch (err) {
this.state.templateLoadFailed = true;
this.Notifications.error('Failure', err, 'Unable to retrieve custom template data');
}
this.formValues.network = _.find(this.availableNetworks, function (o) {
return o.Name === 'bridge';
});
@ -213,9 +220,6 @@ class CustomTemplatesViewController {
this.$anchorScroll('view-top');
const applicationState = this.StateManager.getState();
this.state.deployable = this.isDeployable(applicationState.endpoint, template.Type);
const file = await this.CustomTemplateService.customTemplateFile(template.Id);
this.state.templateContent = file;
this.formValues.fileContent = file;
if (template.Variables && template.Variables.length > 0) {
const variables = Object.fromEntries(template.Variables.map((variable) => [variable.name, '']));

View file

@ -7,6 +7,22 @@
<form class="form-horizontal" name="customTemplateForm">
<custom-template-common-fields form-values="$ctrl.formValues" show-platform-field="true" show-type-field="true"></custom-template-common-fields>
<git-form value="$ctrl.formValues" on-change="($ctrl.handleChange)" ng-if="$ctrl.formValues.GitConfig"></git-form>
<div class="form-group">
<div class="col-sm-12"
><button type="button" class="btn btn-sm btn-light !ml-0" ng-if="$ctrl.formValues.GitConfig" ng-click="$ctrl.previewFileFromGitRepository()">
<pr-icon icon="'refresh-cw'" feather="true"></pr-icon>Reload custom template</button
>
</div>
<div class="col-sm-12" ng-if="$ctrl.state.templatePreviewFailed">
<p class="small vertical-center text-danger mt-5">
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>
Custom template could not be loaded, {{ $ctrl.state.templatePreviewError }}.</p
>
</div>
</div>
<!-- web-editor -->
<web-editor-form
identifier="custom-template-creation-editor"
@ -15,6 +31,7 @@
ng-required="true"
yml="true"
placeholder="Define or paste the content of your docker compose file here"
read-only="$ctrl.state.isEditorReadOnly"
>
<editor-description>
<p>
@ -42,7 +59,11 @@
is-variables-names-from-parent="true"
></custom-templates-variables-definition-field>
<por-access-control-form form-data="$ctrl.formValues.AccessControlData" resource-control="$ctrl.formValues.ResourceControl"></por-access-control-form>
<por-access-control-form
form-data="$ctrl.formValues.AccessControlData"
resource-control="$ctrl.formValues.ResourceControl"
ng-if="$ctrl.formValues.AccessControlData"
></por-access-control-form>
<div class="col-sm-12 form-section-title"> Actions </div>
<div class="form-group">

View file

@ -1,4 +1,5 @@
import _ from 'lodash';
import { getFilePreview } from '@/react/portainer/gitops/gitops.service';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
@ -13,11 +14,18 @@ class EditCustomTemplateViewController {
this.isTemplateVariablesEnabled = isBE;
this.formValues = null;
this.formValues = {
Variables: [],
TLSSkipVerify: false,
};
this.state = {
formValidationError: '',
isEditorDirty: false,
isTemplateValid: true,
isEditorReadOnly: false,
templateLoadFailed: false,
templatePreviewFailed: false,
templatePreviewError: '',
};
this.templates = [];
@ -28,6 +36,7 @@ class EditCustomTemplateViewController {
this.editorUpdate = this.editorUpdate.bind(this);
this.onVariablesChange = this.onVariablesChange.bind(this);
this.handleChange = this.handleChange.bind(this);
this.previewFileFromGitRepository = this.previewFileFromGitRepository.bind(this);
}
getTemplate() {
@ -35,14 +44,25 @@ class EditCustomTemplateViewController {
}
async getTemplateAsync() {
try {
const [template, file] = await Promise.all([
this.CustomTemplateService.customTemplate(this.$state.params.id),
this.CustomTemplateService.customTemplateFile(this.$state.params.id),
]);
template.FileContent = file;
const template = await this.CustomTemplateService.customTemplate(this.$state.params.id);
if (template.GitConfig !== null) {
this.state.isEditorReadOnly = true;
}
try {
template.FileContent = await this.CustomTemplateService.customTemplateFile(this.$state.params.id, template.GitConfig !== null);
} catch (err) {
this.state.templateLoadFailed = true;
throw err;
}
template.Variables = template.Variables || [];
this.formValues = template;
this.formValues = { ...this.formValues, ...template };
this.parseTemplate(template.FileContent);
this.parseGitConfig(template.GitConfig);
this.oldFileContent = this.formValues.FileContent;
if (template.ResourceControl) {
@ -145,6 +165,62 @@ class EditCustomTemplateViewController {
}
}
parseGitConfig(config) {
if (config === null) {
return;
}
let flatConfig = {
RepositoryURL: config.URL,
RepositoryReferenceName: config.ReferenceName,
ComposeFilePathInRepository: config.ConfigFilePath,
RepositoryAuthentication: config.Authentication !== null,
TLSSkipVerify: config.TLSSkipVerify,
};
if (config.Authentication) {
flatConfig = {
...flatConfig,
RepositoryUsername: config.Authentication.Username,
RepositoryPassword: config.Authentication.Password,
};
}
this.formValues = { ...this.formValues, ...flatConfig };
}
previewFileFromGitRepository() {
this.state.templatePreviewFailed = false;
this.state.templatePreviewError = '';
let creds = {};
if (this.formValues.RepositoryAuthentication) {
creds = {
username: this.formValues.RepositoryUsername,
password: this.formValues.RepositoryPassword,
};
}
const payload = {
repository: this.formValues.RepositoryURL,
targetFile: this.formValues.ComposeFilePathInRepository,
tlsSkipVerify: this.formValues.TLSSkipVerify,
...creds,
};
this.$async(async () => {
try {
this.formValues.FileContent = await getFilePreview(payload);
this.state.isEditorDirty = true;
// check if the template contains mustache template symbol
this.parseTemplate(this.formValues.FileContent);
} catch (err) {
this.state.templatePreviewError = err.message;
this.state.templatePreviewFailed = true;
}
});
}
async uiCanExit() {
if (this.formValues.FileContent !== this.oldFileContent && this.state.isEditorDirty) {
return confirmWebEditorDiscard();

View file

@ -39,7 +39,6 @@ angular
$scope.stackWebhookFeature = FeatureId.STACK_WEBHOOK;
$scope.buildMethods = [editor, upload, git, customTemplate];
$scope.STACK_NAME_VALIDATION_REGEX = STACK_NAME_VALIDATION_REGEX;
$scope.isAdmin = Authentication.isAdmin();
$scope.formValues = {
Name: '',
@ -72,6 +71,13 @@ angular
selectedTemplateId: null,
baseWebhookUrl: baseStackWebhookUrl(),
webhookId: createWebhookId(),
templateLoadFailed: false,
isEditorReadOnly: false,
};
$scope.currentUser = {
isAdmin: false,
id: null,
};
$window.onbeforeunload = () => {
@ -296,9 +302,15 @@ angular
$scope.state.selectedTemplateId = templateId;
$scope.state.selectedTemplate = template;
const fileContent = await CustomTemplateService.customTemplateFile(templateId);
$scope.state.templateContent = fileContent;
onChangeFileContent(fileContent);
try {
$scope.state.templateContent = await this.CustomTemplateService.customTemplateFile(templateId, template.GitConfig !== null);
onChangeFileContent($scope.state.templateContent);
$scope.state.isEditorReadOnly = true;
} catch (err) {
$scope.state.templateLoadFailed = true;
throw err;
}
if (template.Variables && template.Variables.length > 0) {
const variables = Object.fromEntries(template.Variables.map((variable) => [variable.name, '']));
@ -321,6 +333,9 @@ angular
}
async function initView() {
$scope.currentUser.isAdmin = Authentication.isAdmin();
$scope.currentUser.id = Authentication.getUserDetails().ID;
var endpointMode = $scope.applicationState.endpoint.mode;
$scope.state.StackType = 2;
$scope.isDockerStandalone = endpointMode.provider === 'DOCKER_STANDALONE';

View file

@ -91,19 +91,31 @@
></git-form>
<div ng-show="state.Method === 'template'">
<custom-template-selector
new-template-path="docker.templates.custom.new"
stack-type="state.StackType"
on-change="(onChangeTemplateId)"
value="state.selectedTemplateId"
></custom-template-selector>
<div class="col-sm-12">
<custom-template-selector
new-template-path="docker.templates.custom.new"
stack-type="state.StackType"
on-change="(onChangeTemplateId)"
value="state.selectedTemplateId"
></custom-template-selector>
<custom-templates-variables-field
ng-if="isTemplateVariablesEnabled && state.selectedTemplate"
definitions="state.selectedTemplate.Variables"
value="formValues.Variables"
on-change="(onChangeTemplateVariables)"
></custom-templates-variables-field>
<custom-templates-variables-field
ng-if="isTemplateVariablesEnabled && state.selectedTemplate"
definitions="state.selectedTemplate.Variables"
value="formValues.Variables"
on-change="(onChangeTemplateVariables)"
></custom-templates-variables-field>
<span ng-if="state.Method === 'template' && state.selectedTemplateId && state.templateLoadFailed">
<p class="small vertical-center text-danger mb-5" ng-if="currentUser.isAdmin || currentUser.id === state.selectedTemplate.CreatedByUserId">
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>Custom template could not be loaded, please
<a ui-sref="docker.templates.custom.edit({id: state.selectedTemplateId})">click here</a> for configuration.</p
>
<p class="small vertical-center text-danger mb-5" ng-if="!(currentUser.isAdmin || currentUser.id === state.selectedTemplate.CreatedByUserId)">
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>Custom template could not be loaded, please contact your administrator.</p
>
</span>
</div>
</div>
<web-editor-form
@ -114,6 +126,7 @@
ng-required="true"
yml="true"
placeholder="Define or paste the content of your docker compose file here"
read-only="state.isEditorReadOnly"
>
<editor-description>
<p>

View file

@ -0,0 +1,25 @@
import axios, { parseAxiosError } from '@/portainer/services/axios';
interface PreviewPayload {
repository: string;
targetFile: string;
reference?: string;
username?: string;
password?: string;
tlsSkipVerify?: boolean;
}
interface PreviewResponse {
FileContent: string;
}
export async function getFilePreview(payload: PreviewPayload) {
try {
const {
data: { FileContent },
} = await axios.post<PreviewResponse>('/gitops/repo/file/preview', payload);
return FileContent;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to fetch file from git');
}
}

View file

@ -8,6 +8,7 @@ interface SearchPayload {
reference?: string;
username?: string;
password?: string;
tlsSkipVerify?: boolean;
}
export function useSearch(payload: SearchPayload, enabled: boolean) {