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

feat(edge/templates): introduce custom templates [EE-6208] (#10561)

This commit is contained in:
Chaim Lev-Ari 2023-11-15 10:45:07 +02:00 committed by GitHub
parent a0f583a17d
commit 68950fbb24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
81 changed files with 2047 additions and 334 deletions

View file

@ -62,7 +62,7 @@ angular
const stacksNew = {
name: 'edge.stacks.new',
url: '/new',
url: '/new?templateId',
views: {
'content@': {
component: 'createEdgeStackView',
@ -150,6 +150,44 @@ angular
component: 'edgeAppTemplatesView',
},
},
data: {
docs: '/user/edge/templates',
},
});
$stateRegistryProvider.register({
name: 'edge.templates.custom',
url: '/custom',
views: {
'content@': {
component: 'edgeCustomTemplatesView',
},
},
data: {
docs: '/user/edge/templates/custom',
},
});
$stateRegistryProvider.register({
name: 'edge.templates.custom.new',
url: '/new?appTemplateId&type',
views: {
'content@': {
component: 'edgeCreateCustomTemplatesView',
},
},
});
$stateRegistryProvider.register({
name: 'edge.templates.custom.edit',
url: '/:templateId',
views: {
'content@': {
component: 'edgeEditCustomTemplatesView',
},
},
});
$stateRegistryProvider.register(edge);

View file

@ -13,6 +13,7 @@ import { withUIRouter } from '@/react-tools/withUIRouter';
import { EdgeGroupAssociationTable } from '@/react/edge/components/EdgeGroupAssociationTable';
import { AssociatedEdgeEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeEnvironmentsSelector';
import { EnvironmentsDatatable } from '@/react/edge/edge-stacks/ItemView/EnvironmentsDatatable';
import { TemplateFieldset } from '@/react/edge/edge-stacks/CreateView/TemplateFieldset';
export const componentsModule = angular
.module('portainer.edge.react.components', [])
@ -99,4 +100,8 @@ export const componentsModule = angular
'onChange',
'value',
])
)
.component(
'edgeStackCreateTemplateFieldset',
r2a(withReactQuery(TemplateFieldset), ['onChange', 'value', 'onChangeFile'])
).name;

View file

@ -3,12 +3,26 @@ import angular from 'angular';
import { r2a } from '@/react-tools/react2angular';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { ListView } from '@/react/edge/templates/custom-templates/ListView';
import { CreateView } from '@/react/edge/templates/custom-templates/CreateView';
import { EditView } from '@/react/edge/templates/custom-templates/EditView';
import { AppTemplatesView } from '@/react/edge/templates/AppTemplatesView';
export const templatesModule = angular
.module('portainer.app.react.components.templates', [])
.component(
'edgeAppTemplatesView',
r2a(withCurrentUser(withUIRouter(AppTemplatesView)), [])
)
.component(
'edgeCustomTemplatesView',
r2a(withCurrentUser(withUIRouter(ListView)), [])
)
.component(
'edgeCreateCustomTemplatesView',
r2a(withCurrentUser(withUIRouter(CreateView)), [])
)
.component(
'edgeEditCustomTemplatesView',
r2a(withCurrentUser(withUIRouter(EditView)), [])
).name;

View file

@ -1,11 +0,0 @@
import angular from 'angular';
angular.module('portainer.edge').factory('EdgeTemplates', function EdgeStacksFactory($resource, API_ENDPOINT_EDGE_TEMPLATES) {
return $resource(
API_ENDPOINT_EDGE_TEMPLATES,
{},
{
query: { method: 'GET', isArray: true },
}
);
});

View file

@ -1,23 +0,0 @@
import angular from 'angular';
class EdgeTemplateService {
/* @ngInject */
constructor(EdgeTemplates) {
this.EdgeTemplates = EdgeTemplates;
}
edgeTemplates() {
return this.EdgeTemplates.query().$promise;
}
async edgeTemplate(template) {
const response = await fetch(template.stackFile);
if (!response.ok) {
throw new Error(response.statusText);
}
return response.text();
}
}
angular.module('portainer.edge').service('EdgeTemplateService', EdgeTemplateService);

View file

@ -5,11 +5,14 @@ import { confirmWebEditorDiscard } from '@@/modals/confirm';
import { baseEdgeStackWebhookUrl } from '@/portainer/helpers/webhookHelper';
import { EnvironmentType } from '@/react/portainer/environments/types';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { getCustomTemplate } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplate';
import { notifyError } from '@/portainer/services/notifications';
import { getCustomTemplateFile } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplateFile';
export default class CreateEdgeStackViewController {
/* @ngInject */
constructor($state, $window, EdgeStackService, EdgeGroupService, EdgeTemplateService, Notifications, FormHelper, $async, $scope) {
Object.assign(this, { $state, $window, EdgeStackService, EdgeGroupService, EdgeTemplateService, Notifications, FormHelper, $async, $scope });
constructor($state, $window, EdgeStackService, EdgeGroupService, Notifications, FormHelper, $async, $scope) {
Object.assign(this, { $state, $window, EdgeStackService, EdgeGroupService, Notifications, FormHelper, $async, $scope });
this.formValues = {
Name: '',
@ -41,6 +44,8 @@ export default class CreateEdgeStackViewController {
hasKubeEndpoint: false,
endpointTypes: [],
baseWebhookUrl: baseEdgeStackWebhookUrl(),
isEdit: false,
selectedTemplate: null,
};
this.edgeGroups = null;
@ -57,6 +62,16 @@ export default class CreateEdgeStackViewController {
this.hasType = this.hasType.bind(this);
this.onChangeDeploymentType = this.onChangeDeploymentType.bind(this);
this.onEnvVarChange = this.onEnvVarChange.bind(this);
this.onChangeTemplate = this.onChangeTemplate.bind(this);
}
/**
* @param {import('@/react/portainer/templates/custom-templates/types').CustomTemplate} template
*/
onChangeTemplate(template) {
return this.$scope.$evalAsync(() => {
this.state.selectedTemplate = template;
});
}
onEnvVarChange(envVars) {
@ -70,7 +85,7 @@ export default class CreateEdgeStackViewController {
const metadata = { type: methodLabel(this.state.Method), format };
if (metadata.type === 'template') {
metadata.templateName = this.selectedTemplate.title;
metadata.templateName = this.state.selectedTemplate && this.state.selectedTemplate.title;
}
return { metadata };
@ -95,6 +110,18 @@ export default class CreateEdgeStackViewController {
}
}
async preSelectTemplate(templateId) {
try {
this.state.Method = 'template';
const template = await getCustomTemplate(templateId);
this.onChangeTemplate(template);
const fileContent = await getCustomTemplateFile({ id: templateId, git: !!template.GitConfig });
this.formValues.StackFileContent = fileContent;
} catch (e) {
notifyError('Failed loading template', e);
}
}
async $onInit() {
try {
this.edgeGroups = await this.EdgeGroupService.groups();
@ -102,6 +129,11 @@ export default class CreateEdgeStackViewController {
this.Notifications.error('Failure', err, 'Unable to retrieve Edge groups');
}
const templateId = this.$state.params.templateId;
if (templateId) {
this.preSelectTemplate(templateId);
}
this.$window.onbeforeunload = () => {
if (this.state.Method === 'editor' && this.formValues.StackFileContent && this.state.isEditorDirty) {
return '';

View file

@ -57,6 +57,8 @@
ng-if="$ctrl.formValues.DeploymentType == $ctrl.EditorType.Compose"
form-values="$ctrl.formValues"
state="$ctrl.state"
template="$ctrl.state.selectedTemplate"
on-change-template="($ctrl.onChangeTemplate)"
></edge-stacks-docker-compose-form>
<edge-stacks-kube-manifest-form

View file

@ -1,18 +1,14 @@
import { fetchFilePreview } from '@/react/portainer/templates/app-templates/queries/useFetchTemplateInfoMutation';
import { editor, git, edgeStackTemplate, upload } from '@@/BoxSelector/common-options/build-methods';
class DockerComposeFormController {
/* @ngInject */
constructor($async, EdgeTemplateService, Notifications) {
Object.assign(this, { $async, EdgeTemplateService, Notifications });
constructor($async, Notifications) {
Object.assign(this, { $async, Notifications });
this.methodOptions = [editor, upload, git, edgeStackTemplate];
this.selectedTemplate = null;
this.onChangeFileContent = this.onChangeFileContent.bind(this);
this.onChangeFile = this.onChangeFile.bind(this);
this.onChangeTemplate = this.onChangeTemplate.bind(this);
this.onChangeMethod = this.onChangeMethod.bind(this);
this.onChangeFormValues = this.onChangeFormValues.bind(this);
}
@ -29,24 +25,13 @@ class DockerComposeFormController {
onChangeMethod(method) {
this.state.Method = method;
this.formValues.StackFileContent = '';
this.selectedTemplate = null;
}
onChangeTemplate(template) {
return this.$async(async () => {
this.formValues.StackFileContent = '';
try {
const fileContent = await fetchFilePreview(template.id);
this.formValues.StackFileContent = fileContent;
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve Template');
}
});
}
onChangeFileContent(value) {
this.formValues.StackFileContent = value;
this.state.isEditorDirty = true;
return this.$async(async () => {
this.formValues.StackFileContent = value;
this.state.isEditorDirty = true;
});
}
onChangeFile(value) {
@ -54,17 +39,6 @@ class DockerComposeFormController {
this.formValues.StackFile = value;
});
}
async $onInit() {
return this.$async(async () => {
try {
const templates = await this.EdgeTemplateService.edgeTemplates();
this.templates = templates.map((template) => ({ ...template, label: `${template.title} - ${template.description}` }));
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve Templates');
}
});
}
}
export default DockerComposeFormController;

View file

@ -1,14 +1,25 @@
<div class="col-sm-12 form-section-title"> Build method </div>
<box-selector slim="true" radio-name="'method'" value="$ctrl.state.Method" options="$ctrl.methodOptions" on-change="($ctrl.onChangeMethod)"></box-selector>
<!-- template -->
<div ng-if="$ctrl.state.Method === 'template'">
<edge-stack-create-template-fieldset
value="$ctrl.template"
on-change="($ctrl.onChangeTemplate)"
on-change-file="($ctrl.onChangeFileContent)"
></edge-stack-create-template-fieldset>
</div>
<!-- !template -->
<web-editor-form
ng-if="$ctrl.state.Method === 'editor'"
ng-if="$ctrl.state.Method === 'editor' || ($ctrl.state.Method === 'template' && $ctrl.template)"
identifier="stack-creation-editor"
value="$ctrl.formValues.StackFileContent"
on-change="($ctrl.onChangeFileContent)"
ng-required="true"
yml="true"
placeholder="Define or paste the content of your docker compose file here"
read-only="$ctrl.state.Method === 'template' && $ctrl.template.GitConfig"
>
<editor-description>
You can get more information about Compose file format in the
@ -21,51 +32,12 @@
<file-upload-description> You can upload a Compose file from your computer. </file-upload-description>
</file-upload-form>
<git-form
ng-if="$ctrl.state.Method === 'repository'"
value="$ctrl.formValues"
on-change="($ctrl.onChangeFormValues)"
base-webhook-url="{{ $ctrl.state.baseWebhookUrl }}"
webhook-id="{{ $ctrl.state.webhookId }}"
docs-links
></git-form>
<!-- template -->
<div ng-if="$ctrl.state.Method === 'template'">
<div class="form-group">
<label for="stack_template" class="col-sm-1 control-label text-left"> Template </label>
<div class="col-sm-11">
<select
class="form-control"
ng-model="$ctrl.selectedTemplate"
ng-options="template as template.label for template in $ctrl.templates"
ng-change="$ctrl.onChangeTemplate($ctrl.selectedTemplate)"
>
<option value="" label="Select an Edge stack template" disabled selected="selected"> </option>
</select>
</div>
</div>
<!-- description -->
<div ng-if="$ctrl.selectedTemplate.note">
<div class="col-sm-12 form-section-title"> Information </div>
<div class="form-group">
<div class="col-sm-12">
<div class="template-note" ng-bind-html="$ctrl.selectedTemplate.note"></div>
</div>
</div>
</div>
<!-- !description -->
<web-editor-form
ng-if="$ctrl.selectedTemplate && $ctrl.formValues.StackFileContent"
identifier="template-content-editor"
value="$ctrl.formValues.StackFileContent"
on-change="($ctrl.onChangeFileContent)"
yml="true"
placeholder="Define or paste the content of your docker compose file here"
ng-required="true"
>
</web-editor-form>
<!-- !template -->
<div ng-if="$ctrl.state.Method == 'repository'">
<git-form
value="$ctrl.formValues"
on-change="($ctrl.onChangeFormValues)"
base-webhook-url="{{ $ctrl.state.baseWebhookUrl }}"
webhook-id="{{ $ctrl.state.webhookId }}"
docs-links
></git-form>
</div>

View file

@ -7,5 +7,7 @@ export const edgeStacksDockerComposeForm = {
bindings: {
formValues: '=',
state: '=',
template: '<',
onChangeTemplate: '<',
},
};