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:
parent
a0f583a17d
commit
68950fbb24
81 changed files with 2047 additions and 334 deletions
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
);
|
||||
});
|
|
@ -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);
|
|
@ -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 '';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -7,5 +7,7 @@ export const edgeStacksDockerComposeForm = {
|
|||
bindings: {
|
||||
formValues: '=',
|
||||
state: '=',
|
||||
template: '<',
|
||||
onChangeTemplate: '<',
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue