diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 811f71a04..221d4eb1d 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -28,17 +28,15 @@ Briefly describe the problem you are having in a few paragraphs. **Steps to reproduce the issue:** -1. -2. -3. +1. 2. 3. Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead? **Technical details:** -* Portainer version: -* Target Docker version (the host/cluster you manage): -* Platform (windows/linux): -* Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`): -* Target Swarm version (if applicable): -* Browser: +- Portainer version: +- Target Docker version (the host/cluster you manage): +- Platform (windows/linux): +- Command used to start Portainer (`docker run -p 9443:9443 portainer/portainer`): +- Target Swarm version (if applicable): +- Browser: diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 52a031d48..1d950b9ac 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -4,7 +4,6 @@ about: Create a bug report title: '' labels: bug/need-confirmation, kind/bug assignees: '' - --- + {{ $item.Name }} diff --git a/app/edge/components/edge-groups-selector/index.js b/app/edge/components/edge-groups-selector/index.js index 0a7f7cbb3..902139191 100644 --- a/app/edge/components/edge-groups-selector/index.js +++ b/app/edge/components/edge-groups-selector/index.js @@ -3,7 +3,8 @@ import angular from 'angular'; angular.module('portainer.edge').component('edgeGroupsSelector', { templateUrl: './edgeGroupsSelector.html', bindings: { - model: '=', + model: '<', items: '<', + onChange: '<', }, }); diff --git a/app/edge/components/edge-job-form/edgeJobForm.html b/app/edge/components/edge-job-form/edgeJobForm.html index d94f24004..b3d75e3a2 100644 --- a/app/edge/components/edge-job-form/edgeJobForm.html +++ b/app/edge/components/edge-job-form/edgeJobForm.html @@ -75,7 +75,7 @@
- Time should be set according to the chosen endpoints' timezone. + Time should be set according to the chosen environments' timezone.
@@ -130,7 +130,7 @@ />
- Time should be set according to the chosen endpoints' timezone. + Time should be set according to the chosen environments' timezone.
@@ -215,7 +215,7 @@
- Target endpoints + Target environments
- Endpoint + Environment diff --git a/app/edge/components/edge-stack-deployment-type-selector/edge-stack-deployment-type-selector.controller.js b/app/edge/components/edge-stack-deployment-type-selector/edge-stack-deployment-type-selector.controller.js new file mode 100644 index 000000000..3cedd0464 --- /dev/null +++ b/app/edge/components/edge-stack-deployment-type-selector/edge-stack-deployment-type-selector.controller.js @@ -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' : ''; + }, + }, + ]; + } +} diff --git a/app/edge/components/edge-stack-deployment-type-selector/edge-stack-deployment-type-selector.html b/app/edge/components/edge-stack-deployment-type-selector/edge-stack-deployment-type-selector.html new file mode 100644 index 000000000..4eae73cc5 --- /dev/null +++ b/app/edge/components/edge-stack-deployment-type-selector/edge-stack-deployment-type-selector.html @@ -0,0 +1,4 @@ +
+ Deployment type +
+ diff --git a/app/edge/components/edge-stack-deployment-type-selector/index.js b/app/edge/components/edge-stack-deployment-type-selector/index.js new file mode 100644 index 000000000..c175249fd --- /dev/null +++ b/app/edge/components/edge-stack-deployment-type-selector/index.js @@ -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); diff --git a/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatable.html b/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatable.html index 69bb34b98..2cb16bc40 100644 --- a/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatable.html +++ b/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatable.html @@ -57,7 +57,7 @@ Loading... - No endpoint available. + No environment available. diff --git a/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatableController.js b/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatableController.js index 8a0c12765..f71f6d330 100644 --- a/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatableController.js +++ b/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatableController.js @@ -101,7 +101,7 @@ export class EdgeStackEndpointsDatatableController { this.state.filteredDataSet = endpoints; this.state.totalFilteredDataSet = totalCount; } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve endpoints'); + this.Notifications.error('Failure', err, 'Unable to retrieve environments'); } finally { this.state.loading = false; } diff --git a/app/edge/components/edge-stack-status/edgeStackStatus.html b/app/edge/components/edge-stack-status/edgeStackStatus.html index 5ffda22b5..02c8a07d0 100644 --- a/app/edge/components/edge-stack-status/edgeStackStatus.html +++ b/app/edge/components/edge-stack-status/edgeStackStatus.html @@ -1,3 +1,3 @@ -{{ $ctrl.status.acknowledged || 0 }} -{{ $ctrl.status.ok || 0 }} -{{ $ctrl.status.error || 0 }} +{{ $ctrl.status.acknowledged || 0 }} +{{ $ctrl.status.ok || 0 }} +{{ $ctrl.status.error || 0 }} diff --git a/app/edge/components/edit-edge-stack-form/editEdgeStackForm.html b/app/edge/components/edit-edge-stack-form/editEdgeStackForm.html index e21b87c0e..95eb4f316 100644 --- a/app/edge/components/edit-edge-stack-form/editEdgeStackForm.html +++ b/app/edge/components/edit-edge-stack-form/editEdgeStackForm.html @@ -4,52 +4,70 @@
- +
- -
- Web editor -
-
- - You can get more information about Compose file format in the - - official documentation - - . - -
-
+
- +
+ + One or more of the selected Edge group contains Edge Docker endpoints that cannot be used with a Kubernetes Edge stack. +
- -
- Options -
-
+ + +
- - +
+ + Portainer uses Kompose 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. +
+ + +
+ You can get more information about Compose file format in the + + official documentation + + . +
+
+
+ + + +

+ You can get more information about Kubernetes file format in the + official documentation. +

+
+
+
Actions @@ -59,9 +77,7 @@
-

Manually select Edge endpoints

+

Manually select Edge environments

@@ -40,7 +40,7 @@ Dynamic
-

Automatically associate endpoints via tags

+

Automatically associate environments via tags

@@ -49,9 +49,9 @@
- +
- Associated endpoints + Associated environments
-
No Edge endpoints are available. Head over to the Endpoints view to add endpoints.
+
+ No Edge environments are available. Head over to the Environments view to add environments. +
@@ -84,7 +86,7 @@ Partial match
-

Associate any endpoint matching at least one of the selected tags

+

Associate any environment matching at least one of the selected tags

@@ -94,7 +96,7 @@ Full match
-

Associate any endpoint matching all of the selected tags

+

Associate any environment matching all of the selected tags

@@ -106,7 +108,7 @@
- Associated endpoints by tags + Associated environments by tags
- Endpoints Count + Environments Count diff --git a/app/edge/services/edge-stack.js b/app/edge/services/edge-stack.js index 9ec0f20e1..2a6147e45 100644 --- a/app/edge/services/edge-stack.js +++ b/app/edge/services/edge-stack.js @@ -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 }; } diff --git a/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupViewController.js b/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupViewController.js index 196993d66..49c7c05fc 100644 --- a/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupViewController.js +++ b/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupViewController.js @@ -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); diff --git a/app/edge/views/edge-jobs/edgeJob/edgeJobController.js b/app/edge/views/edge-jobs/edgeJob/edgeJobController.js index f88e9576d..190f45fb4 100644 --- a/app/edge/views/edge-jobs/edgeJob/edgeJobController.js +++ b/app/edge/views/edge-jobs/edgeJob/edgeJobController.js @@ -161,7 +161,7 @@ export class EdgeJobController { this.results = results; } } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve endpoint list'); + this.Notifications.error('Failure', err, 'Unable to retrieve environment list'); } this.$window.onbeforeunload = () => { diff --git a/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackViewController.js b/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.controller.js similarity index 57% rename from app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackViewController.js rename to app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.controller.js index a9b229aa4..db92bb610 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackViewController.js +++ b/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.controller.js @@ -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); } } diff --git a/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.html b/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.html new file mode 100644 index 000000000..0aedef00f --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.html @@ -0,0 +1,94 @@ + + + Edge Stacks > Create Edge stack + + +
+
+ + +
+ +
+ +
+ +
+
+ + +
+ Edge Groups +
+
+
+ +
+
+ No Edge groups are available. Head over to the Edge groups view to create one. +
+
+ + + +
+
+
+ + Portainer uses Kompose 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. +
+
+
+ + + + + + +
+ Actions +
+
+
+ + + {{ $ctrl.state.formValidationError }} + +
+
+ +
+
+
+
+
diff --git a/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.js b/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.js new file mode 100644 index 000000000..7da77fb05 --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.js @@ -0,0 +1,6 @@ +import controller from './create-edge-stack-view.controller'; + +export const createEdgeStackView = { + templateUrl: './create-edge-stack-view.html', + controller, +}; diff --git a/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html b/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html deleted file mode 100644 index 916bcc1a7..000000000 --- a/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html +++ /dev/null @@ -1,220 +0,0 @@ - - - Edge Stacks > Create Edge stack - - -
-
- - -
- -
- -
- -
-
- - -
- Edge Groups -
-
-
- -
-
- No Edge groups are available. Head over to the Edge groups view to create one. -
-
- -
- Build method -
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- Web editor -
-
- - You can get more information about Compose file format in the - - official documentation - - . - -
-
-
- -
-
-
- - -
-
- Upload -
-
- - You can upload a Compose file from your computer. - -
-
-
- - - {{ $ctrl.formValues.StackFile.name }} - - -
-
-
- - - - - -
-
- -
- -
-
- -
-
- Information -
-
-
-
-
-
-
- - -
-
- Web editor -
-
-
- -
-
-
-
- - - -
- Actions -
-
-
- - - {{ $ctrl.state.formValidationError }} - -
-
- -
-
-
-
-
diff --git a/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js new file mode 100644 index 000000000..c78caa495 --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js @@ -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; diff --git a/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.html b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.html new file mode 100644 index 000000000..48388c9dc --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.html @@ -0,0 +1,74 @@ +
+ Build method +
+ + + + + You can get more information about Compose file format in the + + official documentation + + . + + + + + + You can upload a Compose file from your computer. + + + + + + +
+
+ +
+ +
+
+ +
+
+ Information +
+
+
+
+
+
+
+ + + + + + +
diff --git a/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/index.js b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/index.js new file mode 100644 index 000000000..59fd0aecc --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/index.js @@ -0,0 +1,11 @@ +import controller from './docker-compose-form.controller.js'; + +export const edgeStacksDockerComposeForm = { + templateUrl: './docker-compose-form.html', + controller, + + bindings: { + formValues: '=', + state: '=', + }, +}; diff --git a/app/edge/views/edge-stacks/createEdgeStackView/index.js b/app/edge/views/edge-stacks/createEdgeStackView/index.js index 29206fe54..af8eee573 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/index.js +++ b/app/edge/views/edge-stacks/createEdgeStackView/index.js @@ -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; diff --git a/app/edge/views/edge-stacks/createEdgeStackView/kube-deploy-description/index.js b/app/edge/views/edge-stacks/createEdgeStackView/kube-deploy-description/index.js new file mode 100644 index 000000000..53d8f3dd5 --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/kube-deploy-description/index.js @@ -0,0 +1,3 @@ +export const kubeDeployDescription = { + templateUrl: './kube-deploy-description.html', +}; diff --git a/app/edge/views/edge-stacks/createEdgeStackView/kube-deploy-description/kube-deploy-description.html b/app/edge/views/edge-stacks/createEdgeStackView/kube-deploy-description/kube-deploy-description.html new file mode 100644 index 000000000..c12d7ab22 --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/kube-deploy-description/kube-deploy-description.html @@ -0,0 +1,8 @@ +

+ + This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...). +

+

+ You can get more information about Kubernetes file format in the + official documentation. +

diff --git a/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/index.js b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/index.js new file mode 100644 index 000000000..41949f7e3 --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/index.js @@ -0,0 +1,11 @@ +import controller from './kube-manifest-form.controller.js'; + +export const kubeManifestForm = { + templateUrl: './kube-manifest-form.html', + controller, + + bindings: { + formValues: '=', + state: '=', + }, +}; diff --git a/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.controller.js b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.controller.js new file mode 100644 index 000000000..5c3719cac --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.controller.js @@ -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; diff --git a/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.html b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.html new file mode 100644 index 000000000..1b09a0712 --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.html @@ -0,0 +1,26 @@ +
+ Build method +
+ + + + + + + + + + + + + + + diff --git a/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackView.html b/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackView.html index 74bab4ec9..d5d5c04b5 100644 --- a/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackView.html +++ b/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackView.html @@ -26,11 +26,11 @@
- Endpoints + Environments
{ const status = this.stack.Status[endpoint.Id]; @@ -108,7 +108,7 @@ export class EditEdgeStackViewController { }); return { endpoints, totalCount }; } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve endpoint information'); + this.Notifications.error('Failure', err, 'Unable to retrieve environment information'); } } } diff --git a/app/edge/views/edge-stacks/index.js b/app/edge/views/edge-stacks/index.js new file mode 100644 index 000000000..ed21f54f6 --- /dev/null +++ b/app/edge/views/edge-stacks/index.js @@ -0,0 +1,5 @@ +import angular from 'angular'; + +import createModule from './createEdgeStackView'; + +export default angular.module('portainer.edge.stacks', [createModule]).name; diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js index 3e854bead..853c7a641 100644 --- a/app/kubernetes/__module.js +++ b/app/kubernetes/__module.js @@ -38,13 +38,33 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo await KubernetesNamespaceService.get(); } catch (e) { - Notifications.error('Failed loading endpoint', e); + Notifications.error('Failed loading environment', e); $state.go('portainer.home', {}, { reload: true }); } }); }, }; + const helmApplication = { + name: 'kubernetes.helm', + url: '/helm/:namespace/:name', + views: { + 'content@': { + component: 'kubernetesHelmApplicationView', + }, + }, + }; + + const helmTemplates = { + name: 'kubernetes.templates.helm', + url: '/helm', + views: { + 'content@': { + component: 'helmTemplatesView', + }, + }, + }; + const applications = { name: 'kubernetes.applications', url: '/applications', @@ -301,6 +321,8 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo }; $stateRegistryProvider.register(kubernetes); + $stateRegistryProvider.register(helmApplication); + $stateRegistryProvider.register(helmTemplates); $stateRegistryProvider.register(applications); $stateRegistryProvider.register(applicationCreation); $stateRegistryProvider.register(application); diff --git a/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.controller.js b/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.controller.js new file mode 100644 index 000000000..b20b42ec9 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.controller.js @@ -0,0 +1,12 @@ +import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; + +export default class { + $onInit() { + const secrets = (this.configurations || []) + .filter((config) => config.Data && config.Type === KubernetesConfigurationTypes.SECRET) + .flatMap((config) => Object.entries(config.Data)) + .map(([key, value]) => ({ key, value })); + + this.state = { secrets }; + } +} diff --git a/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.html b/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.html new file mode 100644 index 000000000..7d9cc659e --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.html @@ -0,0 +1,12 @@ +
+ Secrets +
+ + + + + + +
+ +
diff --git a/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.js b/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.js new file mode 100644 index 000000000..410563560 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.js @@ -0,0 +1,10 @@ +import angular from 'angular'; +import controller from './applications-datatable-details.controller'; + +angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatableDetails', { + templateUrl: './applications-datatable-details.html', + controller, + bindings: { + configurations: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.css b/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.css new file mode 100644 index 000000000..17c283bf7 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.css @@ -0,0 +1,10 @@ +.published-url-container { + display: grid; + grid-template-columns: 1fr 1fr 3fr; + padding-top: 1rem; + padding-bottom: 0.5rem; +} + +.publish-url-link { + width: min-content; +} diff --git a/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.html b/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.html new file mode 100644 index 000000000..ea25b014c --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.html @@ -0,0 +1,6 @@ +
+
+ Published URL +
+ {{ $ctrl.publishedUrl }} +
diff --git a/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.js b/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.js new file mode 100644 index 000000000..7177b851b --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.js @@ -0,0 +1,9 @@ +import angular from 'angular'; +import './applications-datatable-url.css'; + +angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatableUrl', { + templateUrl: './applications-datatable-url.html', + bindings: { + publishedUrl: '@', + }, +}); diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.css b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.css new file mode 100644 index 000000000..6cf9da69a --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.css @@ -0,0 +1,16 @@ +.secondary-heading { + background-color: #e7f6ff !important; +} + +.secondary-body { + background-color: #f1f9fd; +} + +.datatable-wide { + width: 55px; +} + +.datatable-padding-vertical { + padding-top: 1.5rem; + padding-bottom: 1.5rem; +} diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html index 3a2517be6..1f1d46fdd 100644 --- a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html @@ -1,7 +1,7 @@
-
+
{{ $ctrl.titleText }}
@@ -62,7 +62,7 @@
-
+
-