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

feat(edgestacks): support kubernetes edge stacks (#5276) [EE-393]

This commit is contained in:
Chaim Lev-Ari 2021-09-09 11:38:34 +03:00 committed by GitHub
parent 79ca51c92e
commit 5c8450c4c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 1466 additions and 521 deletions

View file

@ -1,6 +1,8 @@
import angular from 'angular';
angular.module('portainer.edge', []).config(function config($stateRegistryProvider) {
import edgeStackModule from './views/edge-stacks';
angular.module('portainer.edge', [edgeStackModule]).config(function config($stateRegistryProvider) {
const edge = {
name: 'edge',
url: '/edge',

View file

@ -1,4 +1,12 @@
<ui-select multiple ng-model="$ctrl.model" close-on-select="false" data-cy="edgeGroupCreate-edgeGroupsSelector">
<!-- on-select/on-remove are called with model because ui-select uses 2-way-binding -->
<ui-select
multiple
ng-model="$ctrl.model"
close-on-select="false"
on-select="$ctrl.onChange($ctrl.model)"
on-remove="$ctrl.onChange($ctrl.model)"
data-cy="edgeGroupCreate-edgeGroupsSelector"
>
<ui-select-match placeholder="Select one or multiple group(s)">
<span>
{{ $item.Name }}

View file

@ -3,7 +3,8 @@ import angular from 'angular';
angular.module('portainer.edge').component('edgeGroupsSelector', {
templateUrl: './edgeGroupsSelector.html',
bindings: {
model: '=',
model: '<',
items: '<',
onChange: '<',
},
});

View file

@ -0,0 +1,21 @@
export default class EdgeStackDeploymentTypeSelectorController {
/* @ngInject */
constructor() {
this.deploymentOptions = [
{ id: 'deployment_compose', icon: 'fab fa-docker', label: 'Compose', description: 'docker-compose format', value: 0 },
{
id: 'deployment_kube',
icon: 'fa fa-cubes',
label: 'Kubernetes',
description: 'Kubernetes manifest format',
value: 1,
disabled: () => {
return this.hasDockerEndpoint();
},
tooltip: () => {
return this.hasDockerEndpoint() ? 'Cannot use this option with Edge Docker endpoints' : '';
},
},
];
}
}

View file

@ -0,0 +1,4 @@
<div class="col-sm-12 form-section-title">
Deployment type
</div>
<box-selector radio-name="deploymentType" ng-model="$ctrl.value" options="$ctrl.deploymentOptions" on-change="($ctrl.onChange)"></box-selector>

View file

@ -0,0 +1,15 @@
import angular from 'angular';
import controller from './edge-stack-deployment-type-selector.controller.js';
export const edgeStackDeploymentTypeSelector = {
templateUrl: './edge-stack-deployment-type-selector.html',
controller,
bindings: {
value: '<',
onChange: '<',
hasDockerEndpoint: '<',
},
};
angular.module('portainer.edge').component('edgeStackDeploymentTypeSelector', edgeStackDeploymentTypeSelector);

View file

@ -4,52 +4,70 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<edge-groups-selector model="$ctrl.model.EdgeGroups" items="$ctrl.edgeGroups"></edge-groups-selector>
<edge-groups-selector model="$ctrl.model.EdgeGroups" items="$ctrl.edgeGroups" on-change="($ctrl.onChangeGroups)"></edge-groups-selector>
</div>
</div>
<!-- web-editor -->
<div class="col-sm-12 form-section-title">
Web editor
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can get more information about Compose file format in the
<a href="https://docs.docker.com/compose/compose-file/" target="_blank">
official documentation
</a>
.
</span>
</div>
<div class="form-group">
<div class="form-group" ng-if="!$ctrl.validateEndpointsForDeployment()">
<div class="col-sm-12">
<code-editor
value="$ctrl.model.StackFileContent"
identifier="stack-creation-editor"
placeholder="# Define or paste the content of your docker-compose file here"
yml="true"
on-change="($ctrl.editorUpdate)"
></code-editor>
<div class="small text-muted space-right text-warning">
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i>
One or more of the selected Edge group contains Edge Docker endpoints that cannot be used with a Kubernetes Edge stack.
</div>
</div>
</div>
<!-- !web-editor -->
<div class="col-sm-12 form-section-title">
Options
</div>
<div class="form-group">
<edge-stack-deployment-type-selector
value="$ctrl.model.DeploymentType"
has-docker-endpoint="$ctrl.hasDockerEndpoint"
on-change="($ctrl.onChangeDeploymentType)"
></edge-stack-deployment-type-selector>
<div class="form-group" ng-if="$ctrl.model.DeploymentType === 0 && $ctrl.hasKubeEndpoint()">
<div class="col-sm-12">
<label for="EnablePrune" class="control-label text-left">
Prune services
<portainer-tooltip position="bottom" message="Prune services that are not longer referenced in the stack file."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="EnablePrune" ng-model="$ctrl.model.Prune" />
<i></i>
</label>
<div class="small text-muted space-right">
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i>
Portainer uses <a href="https://kompose.io/" target="_blank">Kompose</a> to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that not all the
Compose format options are supported by Kompose at the moment.
</div>
</div>
</div>
<web-editor-form
ng-if="$ctrl.model.DeploymentType === 0"
value="$ctrl.model.StackFileContent"
yml="true"
identifier="compose-editor"
placeholder="# Define or paste the content of your docker-compose file here"
on-change="($ctrl.onChangeComposeConfig)"
>
<editor-description>
<div>
You can get more information about Compose file format in the
<a href="https://docs.docker.com/compose/compose-file/" target="_blank">
official documentation
</a>
.
</div>
</editor-description>
</web-editor-form>
<web-editor-form
ng-if="$ctrl.model.DeploymentType === 1"
value="$ctrl.model.StackFileContent"
yml="true"
identifier="kube-manifest-editor"
placeholder="# Define or paste the content of your manifest here"
on-change="($ctrl.onChangeKubeManifest)"
>
<editor-description>
<p>
You can get more information about Kubernetes file format in the
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/" target="_blank">official documentation</a>.
</p>
</editor-description>
</web-editor-form>
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
@ -59,9 +77,7 @@
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.actionInProgress
|| !$ctrl.model.EdgeGroups.length
|| !$ctrl.model.StackFileContent"
ng-disabled="$ctrl.actionInProgress || !$ctrl.isFormValid()"
ng-click="$ctrl.submitAction()"
button-spinner="$ctrl.actionInProgress"
>

View file

@ -1,13 +1,83 @@
import { PortainerEndpointTypes } from '@/portainer/models/endpoint/models';
export class EditEdgeStackFormController {
/* @ngInject */
constructor() {
this.editorUpdate = this.editorUpdate.bind(this);
this.state = {
endpointTypes: [],
};
this.fileContents = {
0: '',
1: '',
};
this.onChangeGroups = this.onChangeGroups.bind(this);
this.onChangeFileContent = this.onChangeFileContent.bind(this);
this.onChangeComposeConfig = this.onChangeComposeConfig.bind(this);
this.onChangeKubeManifest = this.onChangeKubeManifest.bind(this);
this.hasDockerEndpoint = this.hasDockerEndpoint.bind(this);
this.hasKubeEndpoint = this.hasKubeEndpoint.bind(this);
this.onChangeDeploymentType = this.onChangeDeploymentType.bind(this);
this.removeLineBreaks = this.removeLineBreaks.bind(this);
this.onChangeFileContent = this.onChangeFileContent.bind(this);
}
editorUpdate(cm) {
if (this.model.StackFileContent.replace(/(\r\n|\n|\r)/gm, '') !== cm.getValue().replace(/(\r\n|\n|\r)/gm, '')) {
this.model.StackFileContent = cm.getValue();
hasKubeEndpoint() {
return this.state.endpointTypes.includes(PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment);
}
hasDockerEndpoint() {
return this.state.endpointTypes.includes(PortainerEndpointTypes.EdgeAgentOnDockerEnvironment);
}
onChangeGroups(groups) {
this.model.EdgeGroups = groups;
this.checkEndpointTypes(groups);
}
isFormValid() {
return this.model.EdgeGroups.length && this.model.StackFileContent && this.validateEndpointsForDeployment();
}
checkEndpointTypes(groups) {
const edgeGroups = groups.map((id) => this.edgeGroups.find((e) => e.Id === id));
this.state.endpointTypes = edgeGroups.flatMap((group) => group.EndpointTypes);
}
removeLineBreaks(value) {
return value.replace(/(\r\n|\n|\r)/gm, '');
}
onChangeFileContent(type, value) {
const oldValue = this.fileContents[type];
if (this.removeLineBreaks(oldValue) !== this.removeLineBreaks(value)) {
this.model.StackFileContent = value;
this.fileContents[type] = value;
this.isEditorDirty = true;
}
}
onChangeKubeManifest(value) {
this.onChangeFileContent(1, value);
}
onChangeComposeConfig(value) {
this.onChangeFileContent(0, value);
}
onChangeDeploymentType(deploymentType) {
this.model.DeploymentType = deploymentType;
this.model.StackFileContent = this.fileContents[deploymentType];
}
validateEndpointsForDeployment() {
return this.model.DeploymentType == 0 || !this.hasDockerEndpoint();
}
$onInit() {
this.checkEndpointTypes(this.model.EdgeGroups);
}
}

View file

@ -49,7 +49,7 @@ export class EdgeGroupFormController {
async getDynamicEndpointsAsync() {
const { pageNumber, limit, search } = this.endpoints.state;
const start = (pageNumber - 1) * limit + 1;
const query = { search, types: [4], tagIds: this.model.TagIds, tagsPartialMatch: this.model.PartialMatch };
const query = { search, types: [4, 7], tagIds: this.model.TagIds, tagsPartialMatch: this.model.PartialMatch };
const response = await this.EndpointService.endpoints(start, limit, query);

View file

@ -25,15 +25,10 @@ angular.module('portainer.edge').factory('EdgeStackService', function EdgeStackS
};
service.updateStack = async function updateStack(id, stack) {
return EdgeStacks.update({ id }, stack);
return EdgeStacks.update({ id }, stack).$promise;
};
service.createStackFromFileContent = async function createStackFromFileContent(name, stackFileContent, edgeGroups) {
var payload = {
Name: name,
StackFileContent: stackFileContent,
EdgeGroups: edgeGroups,
};
service.createStackFromFileContent = async function createStackFromFileContent(payload) {
try {
return await EdgeStacks.create({ method: 'string' }, payload).$promise;
} catch (err) {
@ -41,27 +36,28 @@ angular.module('portainer.edge').factory('EdgeStackService', function EdgeStackS
}
};
service.createStackFromFileUpload = async function createStackFromFileUpload(name, stackFile, edgeGroups) {
service.createStackFromFileUpload = async function createStackFromFileUpload(payload, file) {
try {
return await FileUploadService.createEdgeStack(name, stackFile, edgeGroups);
return await FileUploadService.createEdgeStack(payload, file);
} catch (err) {
throw { msg: 'Unable to create the stack', err };
}
};
service.createStackFromGitRepository = async function createStackFromGitRepository(name, repositoryOptions, edgeGroups) {
var payload = {
Name: name,
RepositoryURL: repositoryOptions.RepositoryURL,
RepositoryReferenceName: repositoryOptions.RepositoryReferenceName,
ComposeFilePathInRepository: repositoryOptions.ComposeFilePathInRepository,
RepositoryAuthentication: repositoryOptions.RepositoryAuthentication,
RepositoryUsername: repositoryOptions.RepositoryUsername,
RepositoryPassword: repositoryOptions.RepositoryPassword,
EdgeGroups: edgeGroups,
};
service.createStackFromGitRepository = async function createStackFromGitRepository(payload, repositoryOptions) {
try {
return await EdgeStacks.create({ method: 'repository' }, payload).$promise;
return await EdgeStacks.create(
{ method: 'repository' },
{
...payload,
RepositoryURL: repositoryOptions.RepositoryURL,
RepositoryReferenceName: repositoryOptions.RepositoryReferenceName,
FilePathInRepository: repositoryOptions.FilePathInRepository,
RepositoryAuthentication: repositoryOptions.RepositoryAuthentication,
RepositoryUsername: repositoryOptions.RepositoryUsername,
RepositoryPassword: repositoryOptions.RepositoryPassword,
}
).$promise;
} catch (err) {
throw { msg: 'Unable to create the stack', err };
}

View file

@ -1,18 +1,12 @@
export class EditEdgeGroupController {
/* @ngInject */
constructor(EdgeGroupService, GroupService, TagService, Notifications, $state, $async, EndpointService) {
constructor(EdgeGroupService, GroupService, TagService, Notifications, $state, $async) {
this.EdgeGroupService = EdgeGroupService;
this.GroupService = GroupService;
this.TagService = TagService;
this.Notifications = Notifications;
this.$state = $state;
this.$async = $async;
this.EndpointService = EndpointService;
this.state = {
actionInProgress: false,
loaded: false,
};
this.updateGroup = this.updateGroup.bind(this);
this.updateGroupAsync = this.updateGroupAsync.bind(this);

View file

@ -1,6 +1,4 @@
import _ from 'lodash-es';
export class CreateEdgeStackViewController {
export default class CreateEdgeStackViewController {
/* @ngInject */
constructor($state, $window, ModalService, EdgeStackService, EdgeGroupService, EdgeTemplateService, Notifications, FormHelper, $async) {
Object.assign(this, { $state, $window, ModalService, EdgeStackService, EdgeGroupService, EdgeTemplateService, Notifications, FormHelper, $async });
@ -15,8 +13,9 @@ export class CreateEdgeStackViewController {
RepositoryUsername: '',
RepositoryPassword: '',
Env: [],
ComposeFilePathInRepository: 'docker-compose.yml',
ComposeFilePathInRepository: '',
Groups: [],
DeploymentType: 0,
};
this.state = {
@ -25,22 +24,21 @@ export class CreateEdgeStackViewController {
actionInProgress: false,
StackType: null,
isEditorDirty: false,
hasKubeEndpoint: false,
endpointTypes: [],
};
this.edgeGroups = null;
this.createStack = this.createStack.bind(this);
this.createStackAsync = this.createStackAsync.bind(this);
this.validateForm = this.validateForm.bind(this);
this.createStackByMethod = this.createStackByMethod.bind(this);
this.createStackFromFileContent = this.createStackFromFileContent.bind(this);
this.createStackFromFileUpload = this.createStackFromFileUpload.bind(this);
this.createStackFromGitRepository = this.createStackFromGitRepository.bind(this);
this.editorUpdate = this.editorUpdate.bind(this);
this.onChangeTemplate = this.onChangeTemplate.bind(this);
this.onChangeTemplateAsync = this.onChangeTemplateAsync.bind(this);
this.onChangeMethod = this.onChangeMethod.bind(this);
this.onChangeFormValues = this.onChangeFormValues.bind(this);
this.onChangeGroups = this.onChangeGroups.bind(this);
this.hasDockerEndpoint = this.hasDockerEndpoint.bind(this);
this.onChangeDeploymentType = this.onChangeDeploymentType.bind(this);
}
buildAnalyticsProperties() {
@ -67,7 +65,7 @@ export class CreateEdgeStackViewController {
}
}
async uiCanExit() {
uiCanExit() {
if (this.state.Method === 'editor' && this.formValues.StackFileContent && this.state.isEditorDirty) {
return this.ModalService.confirmWebEditorDiscard();
}
@ -81,13 +79,6 @@ export class CreateEdgeStackViewController {
this.Notifications.error('Failure', err, 'Unable to retrieve Edge groups');
}
try {
const templates = await this.EdgeTemplateService.edgeTemplates();
this.templates = _.map(templates, (template) => ({ ...template, label: `${template.title} - ${template.description}` }));
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve Templates');
}
this.$window.onbeforeunload = () => {
if (this.state.Method === 'editor' && this.formValues.StackFileContent && this.state.isEditorDirty) {
return '';
@ -100,52 +91,54 @@ export class CreateEdgeStackViewController {
}
createStack() {
return this.$async(this.createStackAsync);
return this.$async(async () => {
const name = this.formValues.Name;
let method = this.state.Method;
if (method === 'template') {
method = 'editor';
}
if (!this.validateForm(method)) {
return;
}
this.state.actionInProgress = true;
try {
await this.createStackByMethod(name, method);
this.Notifications.success('Stack successfully deployed');
this.state.isEditorDirty = false;
this.$state.go('edge.stacks');
} catch (err) {
this.Notifications.error('Deployment error', err, 'Unable to deploy stack');
} finally {
this.state.actionInProgress = false;
}
});
}
onChangeMethod() {
this.formValues.StackFileContent = '';
this.selectedTemplate = null;
onChangeGroups(groups) {
this.formValues.Groups = groups;
this.checkIfEndpointTypes(groups);
}
onChangeTemplate(template) {
return this.$async(this.onChangeTemplateAsync, template);
}
checkIfEndpointTypes(groups) {
const edgeGroups = groups.map((id) => this.edgeGroups.find((e) => e.Id === id));
this.state.endpointTypes = edgeGroups.flatMap((group) => group.EndpointTypes);
async onChangeTemplateAsync(template) {
this.formValues.StackFileContent = '';
try {
const fileContent = await this.EdgeTemplateService.edgeTemplate(template);
this.formValues.StackFileContent = fileContent;
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve Template');
if (this.hasDockerEndpoint() && this.formValues.DeploymentType == 1) {
this.onChangeDeploymentType(0);
}
}
async createStackAsync() {
const name = this.formValues.Name;
let method = this.state.Method;
hasKubeEndpoint() {
return this.state.endpointTypes.includes(7);
}
if (method === 'template') {
method = 'editor';
}
if (!this.validateForm(method)) {
return;
}
this.state.actionInProgress = true;
try {
await this.createStackByMethod(name, method);
this.Notifications.success('Stack successfully deployed');
this.state.isEditorDirty = false;
this.$state.go('edge.stacks');
} catch (err) {
this.Notifications.error('Deployment error', err, 'Unable to deploy stack');
} finally {
this.state.actionInProgress = false;
}
hasDockerEndpoint() {
return this.state.endpointTypes.includes(4);
}
validateForm(method) {
@ -171,31 +164,55 @@ export class CreateEdgeStackViewController {
}
createStackFromFileContent(name) {
return this.EdgeStackService.createStackFromFileContent(name, this.formValues.StackFileContent, this.formValues.Groups);
const { StackFileContent, Groups, DeploymentType } = this.formValues;
return this.EdgeStackService.createStackFromFileContent({
name,
StackFileContent,
EdgeGroups: Groups,
DeploymentType,
});
}
createStackFromFileUpload(name) {
return this.EdgeStackService.createStackFromFileUpload(name, this.formValues.StackFile, this.formValues.Groups);
const { StackFile, Groups, DeploymentType } = this.formValues;
return this.EdgeStackService.createStackFromFileUpload(
{
Name: name,
EdgeGroups: Groups,
DeploymentType,
},
StackFile
);
}
createStackFromGitRepository(name) {
const { Groups, DeploymentType } = this.formValues;
const repositoryOptions = {
RepositoryURL: this.formValues.RepositoryURL,
RepositoryReferenceName: this.formValues.RepositoryReferenceName,
ComposeFilePathInRepository: this.formValues.ComposeFilePathInRepository,
FilePathInRepository: this.formValues.ComposeFilePathInRepository,
RepositoryAuthentication: this.formValues.RepositoryAuthentication,
RepositoryUsername: this.formValues.RepositoryUsername,
RepositoryPassword: this.formValues.RepositoryPassword,
};
return this.EdgeStackService.createStackFromGitRepository(name, repositoryOptions, this.formValues.Groups);
return this.EdgeStackService.createStackFromGitRepository(
{
name,
EdgeGroups: Groups,
DeploymentType,
},
repositoryOptions
);
}
onChangeFormValues(values) {
this.formValues = values;
onChangeDeploymentType(deploymentType) {
this.formValues.DeploymentType = deploymentType;
this.state.Method = 'editor';
this.formValues.StackFileContent = '';
}
editorUpdate(cm) {
this.formValues.StackFileContent = cm.getValue();
this.state.isEditorDirty = true;
formIsInvalid() {
return this.form.$invalid || !this.formValues.Groups.length || (['template', 'editor'].includes(this.state.Method) && !this.formValues.StackFileContent);
}
}

View file

@ -0,0 +1,94 @@
<rd-header>
<rd-header-title title-text="Create Edge stack"></rd-header-title>
<rd-header-content> <a ui-sref="edge.stacks">Edge Stacks</a> &gt; Create Edge stack </rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="$ctrl.form">
<!-- name-input -->
<div class="form-group">
<label for="stack_name" class="col-sm-1 control-label text-left">
Name
</label>
<div class="col-sm-11">
<input
type="text"
class="form-control"
ng-model="$ctrl.formValues.Name"
id="stack_name"
name="nameField"
placeholder="e.g. mystack"
auto-focus
required
data-cy="edgeStackCreate-nameInput"
/>
</div>
</div>
<!-- !name-input -->
<div class="col-sm-12 form-section-title">
Edge Groups
</div>
<div class="form-group" ng-if="$ctrl.edgeGroups">
<div class="col-sm-12">
<edge-groups-selector ng-if="!$ctrl.noGroups" model="$ctrl.formValues.Groups" on-change="($ctrl.onChangeGroups)" items="$ctrl.edgeGroups"></edge-groups-selector>
</div>
<div ng-if="$ctrl.noGroups" class="col-sm-12 small text-muted">
No Edge groups are available. Head over to the <a ui-sref="edge.groups">Edge groups view</a> to create one.
</div>
</div>
<edge-stack-deployment-type-selector
value="$ctrl.formValues.DeploymentType"
has-docker-endpoint="$ctrl.hasDockerEndpoint"
on-change="($ctrl.onChangeDeploymentType)"
></edge-stack-deployment-type-selector>
<div class="form-group">
<div class="col-sm-12">
<div class="small text-muted space-right" ng-if="$ctrl.formValues.DeploymentType === 0 && $ctrl.hasKubeEndpoint()">
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i>
Portainer uses <a href="https://kompose.io/" target="_blank">Kompose</a> to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that not all
the Compose format options are supported by Kompose at the moment.
</div>
</div>
</div>
<edge-stacks-docker-compose-form ng-if="$ctrl.formValues.DeploymentType == 0" form-values="$ctrl.formValues" state="$ctrl.state"></edge-stacks-docker-compose-form>
<edge-stacks-kube-manifest-form ng-if="$ctrl.formValues.DeploymentType == 1" form-values="$ctrl.formValues" state="$ctrl.state"></edge-stacks-kube-manifest-form>
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.state.actionInProgress || $ctrl.formIsInvalid()"
ng-click="$ctrl.createStack()"
button-spinner="$ctrl.state.actionInProgress"
analytics-on
analytics-event="edge-stack-creation"
analytics-category="edge"
analytics-properties="$ctrl.buildAnalyticsProperties()"
>
<span ng-hide="$ctrl.state.actionInProgress">Deploy the stack</span>
<span ng-show="$ctrl.state.actionInProgress">Deployment in progress...</span>
</button>
<span class="text-danger" ng-if="$ctrl.state.formValidationError" style="margin-left: 5px;">
{{ $ctrl.state.formValidationError }}
</span>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -0,0 +1,6 @@
import controller from './create-edge-stack-view.controller';
export const createEdgeStackView = {
templateUrl: './create-edge-stack-view.html',
controller,
};

View file

@ -1,220 +0,0 @@
<rd-header>
<rd-header-title title-text="Create Edge stack"></rd-header-title>
<rd-header-content> <a ui-sref="edge.stacks">Edge Stacks</a> &gt; Create Edge stack </rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<div class="form-group">
<label for="stack_name" class="col-sm-1 control-label text-left">
Name
</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="$ctrl.formValues.Name" id="stack_name" placeholder="e.g. mystack" auto-focus data-cy="edgeStackCreate-nameInput" />
</div>
</div>
<!-- !name-input -->
<div class="col-sm-12 form-section-title">
Edge Groups
</div>
<div class="form-group" ng-if="$ctrl.edgeGroups">
<div class="col-sm-12">
<edge-groups-selector ng-if="!$ctrl.noGroups" model="$ctrl.formValues.Groups" on-change="(onChangeGroups)" items="$ctrl.edgeGroups"></edge-groups-selector>
</div>
<div ng-if="$ctrl.noGroups" class="col-sm-12 small text-muted">
No Edge groups are available. Head over to the <a ui-sref="edge.groups">Edge groups view</a> to create one.
</div>
</div>
<!-- build-method -->
<div class="col-sm-12 form-section-title">
Build method
</div>
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0;">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="method_editor" ng-model="$ctrl.state.Method" value="editor" ng-change="$ctrl.onChangeMethod()" />
<label for="method_editor" data-cy="edgeStackCreate-webEditorButton">
<div class="boxselector_header">
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
Web editor
</div>
<p>Use our Web editor</p>
</label>
</div>
<div>
<input type="radio" id="method_upload" ng-model="$ctrl.state.Method" value="upload" ng-change="$ctrl.onChangeMethod()" />
<label for="method_upload" data-cy="edgeStackCreate-uploadButton">
<div class="boxselector_header">
<i class="fa fa-upload" aria-hidden="true" style="margin-right: 2px;"></i>
Upload
</div>
<p>Upload from your computer</p>
</label>
</div>
<div>
<input type="radio" id="method_repository" ng-model="$ctrl.state.Method" value="repository" ng-change="$ctrl.onChangeMethod()" />
<label for="method_repository" data-cy="edgeStackCreate-repoButton">
<div class="boxselector_header">
<i class="fab fa-git" aria-hidden="true" style="margin-right: 2px;"></i>
Repository
</div>
<p>Use a git repository</p>
</label>
</div>
<div>
<input type="radio" id="method_template" ng-model="$ctrl.state.Method" value="template" ng-change="$ctrl.onChangeMethod()" />
<label for="method_template" data-cy="edgeStackCreate-templateButton">
<div class="boxselector_header">
<i class="fas fa-rocket" aria-hidden="true" style="margin-right: 2px;"></i>
Template
</div>
<p>Use an Edge stack template</p>
</label>
</div>
</div>
</div>
<!-- !build-method -->
<!-- web-editor -->
<div ng-show="$ctrl.state.Method === 'editor'">
<div class="col-sm-12 form-section-title">
Web editor
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can get more information about Compose file format in the
<a href="https://docs.docker.com/compose/compose-file/" target="_blank">
official documentation
</a>
.
</span>
</div>
<div class="form-group">
<div class="col-sm-12">
<code-editor
identifier="stack-creation-editor"
placeholder="# Define or paste the content of your docker-compose file here"
yml="true"
value="$ctrl.formValues.StackFileContent"
on-change="($ctrl.editorUpdate)"
></code-editor>
</div>
</div>
</div>
<!-- !web-editor -->
<!-- upload -->
<div ng-show="$ctrl.state.Method === 'upload'">
<div class="col-sm-12 form-section-title">
Upload
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can upload a Compose file from your computer.
</span>
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formValues.StackFile">
Select file
</button>
<span style="margin-left: 5px;">
{{ $ctrl.formValues.StackFile.name }}
<i class="fa fa-times red-icon" ng-if="!$ctrl.formValues.StackFile" aria-hidden="true"></i>
</span>
</div>
</div>
</div>
<!-- !upload -->
<!-- repository -->
<git-form ng-show="$ctrl.state.Method === 'repository'" model="$ctrl.formValues" on-change="($ctrl.onChangeFormValues)"></git-form>
<!-- !repository -->
<!-- template -->
<div ng-show="$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 -->
<!-- editor -->
<div ng-if="$ctrl.selectedTemplate && $ctrl.formValues.StackFileContent">
<div class="col-sm-12 form-section-title">
Web editor
</div>
<div class="form-group">
<div class="col-sm-12">
<code-editor
identifier="template-content-editor"
placeholder="# Define or paste the content of your docker-compose file here"
yml="true"
value="$ctrl.formValues.StackFileContent"
on-change="($ctrl.editorUpdate)"
></code-editor>
</div>
</div>
</div>
</div>
<!-- !editor -->
<!-- !template -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.state.actionInProgress || !$ctrl.formValues.Groups.length
|| ($ctrl.state.Method === 'editor' && !$ctrl.formValues.StackFileContent)
|| ($ctrl.state.Method === 'upload' && !$ctrl.formValues.StackFile)
|| ($ctrl.state.Method === 'repository' && ((!$ctrl.formValues.RepositoryURL || !$ctrl.formValues.ComposeFilePathInRepository) || ($ctrl.formValues.RepositoryAuthentication && (!$ctrl.formValues.RepositoryUsername || !$ctrl.formValues.RepositoryPassword))))
|| !$ctrl.formValues.Name"
ng-click="$ctrl.createStack()"
button-spinner="$ctrl.state.actionInProgress"
data-cy="edgeStackCreate-createStackButton"
analytics-on
analytics-event="edge-stack-creation"
analytics-category="edge"
analytics-properties="$ctrl.buildAnalyticsProperties()"
>
<span ng-hide="$ctrl.state.actionInProgress">Deploy the stack</span>
<span ng-show="$ctrl.state.actionInProgress">Deployment in progress...</span>
</button>
<span class="text-danger" ng-if="$ctrl.state.formValidationError" style="margin-left: 5px;">
{{ $ctrl.state.formValidationError }}
</span>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -0,0 +1,64 @@
class DockerComposeFormController {
/* @ngInject */
constructor($async, EdgeTemplateService, Notifications) {
Object.assign(this, { $async, EdgeTemplateService, Notifications });
this.methodOptions = [
{ id: 'method_editor', icon: 'fa fa-edit', label: 'Web editor', description: 'Use our Web editor', value: 'editor' },
{ id: 'method_upload', icon: 'fa fa-upload', label: 'Upload', description: 'Upload from your computer', value: 'upload' },
{ id: 'method_repository', icon: 'fab fa-github', label: 'Repository', description: 'Use a git repository', value: 'repository' },
{ id: 'method_template', icon: 'fa fa-rocket', label: 'Template', description: 'Use an Edge stack template', value: 'template' },
];
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);
}
onChangeFormValues(values) {
this.formValues = values;
}
onChangeMethod() {
this.formValues.StackFileContent = '';
this.selectedTemplate = null;
}
onChangeTemplate(template) {
return this.$async(async () => {
this.formValues.StackFileContent = '';
try {
const fileContent = await this.EdgeTemplateService.edgeTemplate(template);
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;
}
onChangeFile(value) {
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

@ -0,0 +1,74 @@
<div class="col-sm-12 form-section-title">
Build method
</div>
<box-selector radio-name="method" ng-model="$ctrl.state.Method" options="$ctrl.methodOptions" on-change="($ctrl.onChangeMethod)"></box-selector>
<web-editor-form
ng-if="$ctrl.state.Method === 'editor'"
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"
>
<editor-description>
You can get more information about Compose file format in the
<a href="https://docs.docker.com/compose/compose-file/" target="_blank">
official documentation
</a>
.
</editor-description>
</web-editor-form>
<file-upload-form ng-if="$ctrl.state.Method === 'upload'" file="$ctrl.formValues.StackFile" on-change="($ctrl.onChangeFile)" ng-required="true">
<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'" model="$ctrl.formValues" on-change="($ctrl.onChangeFormValues)"></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>

View file

@ -0,0 +1,11 @@
import controller from './docker-compose-form.controller.js';
export const edgeStacksDockerComposeForm = {
templateUrl: './docker-compose-form.html',
controller,
bindings: {
formValues: '=',
state: '=',
},
};

View file

@ -1,8 +1,13 @@
import angular from 'angular';
import { CreateEdgeStackViewController } from './createEdgeStackViewController';
import { createEdgeStackView } from './create-edge-stack-view';
import { edgeStacksDockerComposeForm } from './docker-compose-form';
import { kubeManifestForm } from './kube-manifest-form';
import { kubeDeployDescription } from './kube-deploy-description';
angular.module('portainer.edge').component('createEdgeStackView', {
templateUrl: './createEdgeStackView.html',
controller: CreateEdgeStackViewController,
});
export default angular
.module('portainer.edge.stacks.create', [])
.component('createEdgeStackView', createEdgeStackView)
.component('edgeStacksDockerComposeForm', edgeStacksDockerComposeForm)
.component('edgeStacksKubeManifestForm', kubeManifestForm)
.component('kubeDeployDescription', kubeDeployDescription).name;

View file

@ -0,0 +1,3 @@
export const kubeDeployDescription = {
templateUrl: './kube-deploy-description.html',
};

View file

@ -0,0 +1,8 @@
<p>
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...).
</p>
<p>
You can get more information about Kubernetes file format in the
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/" target="_blank">official documentation</a>.
</p>

View file

@ -0,0 +1,11 @@
import controller from './kube-manifest-form.controller.js';
export const kubeManifestForm = {
templateUrl: './kube-manifest-form.html',
controller,
bindings: {
formValues: '=',
state: '=',
},
};

View file

@ -0,0 +1,29 @@
class KubeManifestFormController {
/* @ngInject */
constructor() {
this.methodOptions = [
{ id: 'method_editor', icon: 'fa fa-edit', label: 'Web editor', description: 'Use our Web editor', value: 'editor' },
{ id: 'method_upload', icon: 'fa fa-upload', label: 'Upload', description: 'Upload from your computer', value: 'upload' },
{ id: 'method_repository', icon: 'fab fa-github', label: 'Repository', description: 'Use a git repository', value: 'repository' },
];
this.onChangeFileContent = this.onChangeFileContent.bind(this);
this.onChangeFormValues = this.onChangeFormValues.bind(this);
this.onChangeFile = this.onChangeFile.bind(this);
}
onChangeFormValues(values) {
this.formValues = values;
}
onChangeFileContent(value) {
this.state.isEditorDirty = true;
this.formValues.StackFileContent = value;
}
onChangeFile(value) {
this.formValues.StackFile = value;
}
}
export default KubeManifestFormController;

View file

@ -0,0 +1,26 @@
<div class="col-sm-12 form-section-title">
Build method
</div>
<box-selector radio-name="method" ng-model="$ctrl.state.Method" options="$ctrl.methodOptions" on-change="($ctrl.onChangeMethod)"></box-selector>
<web-editor-form
ng-if="$ctrl.state.Method === 'editor'"
identifier="stack-creation-editor"
value="$ctrl.formValues.StackFileContent"
on-change="($ctrl.onChangeFileContent)"
yml="true"
placeholder="# Define or paste the content of your manifest here"
ng-required="true"
>
<editor-description>
<kube-deploy-description></kube-deploy-description>
</editor-description>
</web-editor-form>
<file-upload-form ng-if="$ctrl.state.Method === 'upload'" file="$ctrl.formValues.StackFile" on-change="($ctrl.onChangeFile)" ng-required="true">
<file-upload-description>
<kube-deploy-description></kube-deploy-description>
</file-upload-description>
</file-upload-form>
<git-form ng-if="$ctrl.state.Method === 'repository'" deploy-method="kubernetes" model="$ctrl.formValues" on-change="($ctrl.onChangeFormValues)"></git-form>

View file

@ -39,7 +39,7 @@ export class EditEdgeStackViewController {
this.formValues = {
StackFileContent: file,
EdgeGroups: this.stack.EdgeGroups,
Prune: this.stack.Prune,
DeploymentType: this.stack.DeploymentType,
};
this.oldFileContent = this.formValues.StackFileContent;
} catch (err) {
@ -58,7 +58,7 @@ export class EditEdgeStackViewController {
}
async uiCanExit() {
if (this.formValues.StackFileContent !== this.oldFileContent && this.state.isEditorDirty) {
if (this.formValues.StackFileContent.replace(/(\r\n|\n|\r)/gm, '') !== this.oldFileContent.replace(/(\r\n|\n|\r)/gm, '') && this.state.isEditorDirty) {
return this.ModalService.confirmWebEditorDiscard();
}
}
@ -99,7 +99,7 @@ export class EditEdgeStackViewController {
async getPaginatedEndpointsAsync(lastId, limit, search) {
try {
const query = { search, types: [4], endpointIds: this.stackEndpointIds };
const query = { search, types: [4, 7], endpointIds: this.stackEndpointIds };
const { value, totalCount } = await this.EndpointService.endpoints(lastId, limit, query);
const endpoints = _.map(value, (endpoint) => {
const status = this.stack.Status[endpoint.Id];

View file

@ -0,0 +1,5 @@
import angular from 'angular';
import createModule from './createEdgeStackView';
export default angular.module('portainer.edge.stacks', [createModule]).name;

View file

@ -56,7 +56,7 @@ class AssoicatedEndpointsSelectorController {
async getEndpointsAsync() {
const { start, search, limit } = this.getPaginationData('available');
const query = { search, types: [4] };
const query = { search, types: [4, 7] };
const response = await this.EndpointService.endpoints(start, limit, query);
@ -73,7 +73,7 @@ class AssoicatedEndpointsSelectorController {
let response = { value: [], totalCount: 0 };
if (this.endpointIds.length > 0) {
const { start, search, limit } = this.getPaginationData('associated');
const query = { search, types: [4], endpointIds: this.endpointIds };
const query = { search, types: [4, 7], endpointIds: this.endpointIds };
response = await this.EndpointService.endpoints(start, limit, query);
}

View file

@ -1,6 +1,20 @@
<div class="box-selector-item">
<input type="radio" name="{{ $ctrl.radioName }}" id="{{ $ctrl.option.id }}" ng-checked="$ctrl.isChecked($ctrl.option.value)" ng-value="$ctrl.option.value" />
<label for="{{ $ctrl.option.id }}" ng-click="$ctrl.onChange($ctrl.option.value)">
<div
class="box-selector-item"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
tooltip-enable="$ctrl.tooltip"
uib-tooltip="{{ $ctrl.tooltip }}"
>
<input
type="radio"
name="{{ $ctrl.radioName }}"
id="{{ $ctrl.option.id }}"
ng-checked="$ctrl.isChecked($ctrl.option.value)"
ng-value="$ctrl.option.value"
ng-disabled="$ctrl.disabled"
/>
<label for="{{ $ctrl.option.id }}" ng-click="$ctrl.onChange($ctrl.option.value)" t>
<div class="boxselector_header">
<i ng-class="$ctrl.option.icon" aria-hidden="true" style="margin-right: 2px;"></i>
{{ $ctrl.option.label }}

View file

@ -7,5 +7,7 @@ angular.module('portainer.app').component('boxSelectorItem', {
isChecked: '<',
option: '<',
onChange: '<',
disabled: '<',
tooltip: '<',
},
});

View file

@ -14,6 +14,7 @@
font-size: 14px;
margin-bottom: 5px;
font-weight: bold;
user-select: none;
}
.boxselector_header .fa,
@ -46,11 +47,14 @@
box-shadow: var(--shadow-boxselector-color);
position: relative;
}
.box-selector-item input:disabled + label,
.boxselector_wrapper label.boxselector_disabled {
background: var(--bg-boxselector-disabled-color) !important;
border-color: #787878;
color: #787878;
cursor: not-allowed;
pointer-events: none;
}
.boxselector_wrapper input[type='radio']:checked + label {

View file

@ -8,6 +8,8 @@
option="option"
on-change="($ctrl.change)"
is-checked="$ctrl.isChecked"
disabled="option.disabled()"
tooltip="option.tooltip()"
></box-selector-item>
</div>
</div>

View file

@ -1,6 +1,12 @@
<div class="form-group">
<div class="col-sm-12">
<por-switch-field ng-model="$ctrl.model.RepositoryAuthentication" label="Authentication" on-change="($ctrl.onChangeAuth)" data-cy="component-gitAuthToggle"></por-switch-field>
<por-switch-field
ng-model="$ctrl.model.RepositoryAuthentication"
label="Authentication"
name="authSwitch"
on-change="($ctrl.onChangeAuth)"
data-cy="component-gitAuthToggle"
></por-switch-field>
</div>
</div>
<div class="small text-warning" style="margin: 5px 0 15px 0;" ng-if="$ctrl.model.RepositoryAuthentication && $ctrl.showAuthExplanation">
@ -19,6 +25,7 @@
placeholder="git username"
ng-change="$ctrl.onChangeUsername($ctrl.model.RepositoryUsername)"
data-cy="component-gitUsernameInput"
ng-required="$ctrl.model.RepositoryAuthentication"
/>
</div>
</div>

View file

@ -1,11 +1,18 @@
<div class="form-group">
<span class="col-sm-12 text-muted small">
Indicate the path to the Compose file from the root of your repository.
</span>
<span class="col-sm-12 text-muted small"> Indicate the path to the {{ $ctrl.deployMethod == 'compose' ? 'Compose' : 'Manifest' }} file from the root of your repository. </span>
</div>
<div class="form-group">
<label for="stack_repository_path" class="col-sm-2 control-label text-left">Compose path</label>
<label for="stack_repository_path" class="col-sm-2 control-label text-left">{{ $ctrl.deployMethod == 'compose' ? 'Compose' : 'Manifest' }} path</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="$ctrl.value" ng-change="$ctrl.onChange($ctrl.value)" id="stack_repository_path" placeholder="docker-compose.yml" />
<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>

View file

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

View file

@ -5,8 +5,8 @@
</span>
</div>
<div class="form-group">
<label for="stack_repository_reference_name" class="col-sm-1 control-label text-left">Repository reference</label>
<div class="col-sm-11">
<label for="stack_repository_reference_name" class="col-sm-2 control-label text-left">Repository reference</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"

View file

@ -8,12 +8,14 @@
<div class="col-sm-10">
<input
type="text"
name="repoUrlField"
class="form-control"
ng-model="$ctrl.value"
ng-change="$ctrl.onChange($ctrl.value)"
id="stack_repository_url"
placeholder="https://github.com/portainer/portainer-compose"
data-cy="component-gitUrlInput"
required
/>
</div>
</div>

View file

@ -15,4 +15,8 @@ export default class GitFormController {
});
};
}
$onInit() {
this.deployMethod = this.deployMethod || 'compose';
}
}

View file

@ -1,11 +1,18 @@
<div>
<ng-form name="$ctrl.gitForm">
<div class="col-sm-12 form-section-title">
Git repository
</div>
<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)"></git-form-compose-path-field>
<git-form-compose-path-field
value="$ctrl.model.ComposeFilePathInRepository"
on-change="($ctrl.onChangeComposePath)"
deploy-method="{{ $ctrl.deployMethod }}"
></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-auth-fieldset model="$ctrl.model" on-change="($ctrl.onChange)" show-auth-explanation="$ctrl.showAuthExplanation"></git-form-auth-fieldset>
<git-form-auth-fieldset model="$ctrl.model" on-change="($ctrl.onChange)"></git-form-auth-fieldset>
<git-form-auto-update-fieldset ng-if="$ctrl.autoUpdate" model="$ctrl.model" on-change="($ctrl.onChange)"></git-form-auto-update-fieldset>
</div>
</ng-form>

View file

@ -4,6 +4,7 @@ export const gitForm = {
templateUrl: './git-form.html',
controller,
bindings: {
deployMethod: '@',
model: '<',
onChange: '<',
additionalFile: '<',

View file

@ -96,13 +96,13 @@ angular.module('portainer.app').factory('FileUploadService', [
});
};
service.createEdgeStack = function createEdgeStack(stackName, file, edgeGroups) {
service.createEdgeStack = function createEdgeStack({ EdgeGroups, ...payload }, file) {
return Upload.upload({
url: 'api/edge_stacks?method=file',
data: {
file: file,
Name: stackName,
EdgeGroups: Upload.json(edgeGroups),
file,
EdgeGroups: Upload.json(EdgeGroups),
...payload,
},
ignoreLoadingBar: true,
});

View file

@ -16,10 +16,10 @@ angular.module('portainer.app').factory('Notifications', [
service.error = function (title, e, fallbackText) {
var msg = fallbackText;
if (e.err && e.err.data && e.err.data.message) {
msg = e.err.data.message;
} else if (e.err && e.err.data && e.err.data.details) {
if (e.err && e.err.data && e.err.data.details) {
msg = e.err.data.details;
} else if (e.err && e.err.data && e.err.data.message) {
msg = e.err.data.message;
} else if (e.data && e.data.details) {
msg = e.data.details;
} else if (e.data && e.data.message) {
@ -40,6 +40,9 @@ angular.module('portainer.app').factory('Notifications', [
msg = e.msg;
}
// eslint-disable-next-line no-console
console.error(e);
if (msg !== 'Invalid JWT token') {
toastr.error($sanitize(msg), $sanitize(title), { timeOut: 6000 });
}