From d8b88d1004a4ba4bf8ee36cb711f3e5b1768c999 Mon Sep 17 00:00:00 2001
From: Richard Wei <54336863+WaysonWei@users.noreply.github.com>
Date: Fri, 10 Sep 2021 14:25:49 +1200
Subject: [PATCH] feat(wizard):first UX experience for adding environment
EE-1089 (#5581)
* first UX experience for adding environment EE-1089
---
app/portainer/__module.js | 22 ++
.../endpoint-list/endpointList.html | 3 +
app/portainer/services/nameValidator.js | 20 ++
app/portainer/views/auth/authController.js | 2 +-
.../views/init/admin/initAdminController.js | 4 +-
app/portainer/views/wizard/index.js | 7 +
.../views/wizard/wizard-endpoints/index.js | 8 +
.../wizard-endpoint-aci/index.js | 11 +
.../wizard-aci.controller.js | 63 ++++++
.../wizard-endpoint-aci/wizard-aci.html | 67 ++++++
.../wizard-endpoint-docker/index.js | 11 +
.../wizard-docker.controller.js | 211 ++++++++++++++++++
.../wizard-endpoint-docker/wizard-docker.html | 172 ++++++++++++++
.../wizard-endpoint-kubernetes/index.js | 11 +
.../wizard-kubernetes.controller.js | 105 +++++++++
.../wizard-kubernetes.html | 65 ++++++
.../wizard-endpoint-list/index.js | 9 +
.../wizard-endpoint-list.css | 41 ++++
.../wizard-endpoint-list.html | 11 +
.../wizard-endpoints.controller.js | 196 ++++++++++++++++
.../wizard-endpoints/wizard-endpoints.css | 155 +++++++++++++
.../wizard-endpoints/wizard-endpoints.html | 96 ++++++++
.../wizard-endpoints/wizard-stepper/index.js | 9 +
.../wizard-endpoint-type/index.js | 11 +
.../wizard-endpoint-type.html | 12 +
.../wizard-stepper/wizard-stepper.css | 95 ++++++++
.../wizard-stepper/wizard-stepper.html | 6 +
.../views/wizard/wizard-link/index.js | 11 +
.../views/wizard/wizard-link/wizard-link.css | 41 ++++
.../views/wizard/wizard-link/wizard-link.html | 7 +
.../views/wizard/wizard-tls/index.js | 9 +
.../views/wizard/wizard-tls/wizard-tls.html | 49 ++++
.../views/wizard/wizard-view.controller.js | 87 ++++++++
app/portainer/views/wizard/wizard-view.html | 52 +++++
34 files changed, 1676 insertions(+), 3 deletions(-)
create mode 100644 app/portainer/services/nameValidator.js
create mode 100644 app/portainer/views/wizard/index.js
create mode 100644 app/portainer/views/wizard/wizard-endpoints/index.js
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-aci/index.js
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-aci/wizard-aci.controller.js
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-aci/wizard-aci.html
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-docker/index.js
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-docker/wizard-docker.controller.js
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-docker/wizard-docker.html
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-kubernetes/index.js
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-kubernetes/wizard-kubernetes.controller.js
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-kubernetes/wizard-kubernetes.html
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-list/index.js
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-list/wizard-endpoint-list.css
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-list/wizard-endpoint-list.html
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.controller.js
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.css
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.html
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-stepper/index.js
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-stepper/wizard-endpoint-type/index.js
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-stepper/wizard-endpoint-type/wizard-endpoint-type.html
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-stepper/wizard-stepper.css
create mode 100644 app/portainer/views/wizard/wizard-endpoints/wizard-stepper/wizard-stepper.html
create mode 100644 app/portainer/views/wizard/wizard-link/index.js
create mode 100644 app/portainer/views/wizard/wizard-link/wizard-link.css
create mode 100644 app/portainer/views/wizard/wizard-link/wizard-link.html
create mode 100644 app/portainer/views/wizard/wizard-tls/index.js
create mode 100644 app/portainer/views/wizard/wizard-tls/wizard-tls.html
create mode 100644 app/portainer/views/wizard/wizard-view.controller.js
create mode 100644 app/portainer/views/wizard/wizard-view.html
diff --git a/app/portainer/__module.js b/app/portainer/__module.js
index a2432ba9b..3ee842a5a 100644
--- a/app/portainer/__module.js
+++ b/app/portainer/__module.js
@@ -251,6 +251,26 @@ angular.module('portainer.app', ['portainer.oauth', componentsModule, settingsMo
},
};
+ const wizard = {
+ name: 'portainer.wizard',
+ url: '/wizard',
+ views: {
+ 'content@': {
+ component: 'wizardView',
+ },
+ },
+ };
+
+ const wizardEndpoints = {
+ name: 'portainer.wizard.endpoints',
+ url: '/endpoints',
+ views: {
+ 'content@': {
+ component: 'wizardEndpoints',
+ },
+ },
+ };
+
var initEndpoint = {
name: 'portainer.init.endpoint',
url: '/endpoint',
@@ -410,6 +430,8 @@ angular.module('portainer.app', ['portainer.oauth', componentsModule, settingsMo
$stateRegistryProvider.register(groupCreation);
$stateRegistryProvider.register(home);
$stateRegistryProvider.register(init);
+ $stateRegistryProvider.register(wizard);
+ $stateRegistryProvider.register(wizardEndpoints);
$stateRegistryProvider.register(initEndpoint);
$stateRegistryProvider.register(initAdmin);
$stateRegistryProvider.register(registries);
diff --git a/app/portainer/components/endpoint-list/endpointList.html b/app/portainer/components/endpoint-list/endpointList.html
index cd2b2a5a8..7f835a95b 100644
--- a/app/portainer/components/endpoint-list/endpointList.html
+++ b/app/portainer/components/endpoint-list/endpointList.html
@@ -6,6 +6,9 @@
+
Click on an environment to manage
Refresh
diff --git a/app/portainer/services/nameValidator.js b/app/portainer/services/nameValidator.js
new file mode 100644
index 000000000..71dd94be4
--- /dev/null
+++ b/app/portainer/services/nameValidator.js
@@ -0,0 +1,20 @@
+import angular from 'angular';
+
+angular.module('portainer.app').factory('NameValidator', NameValidatorFactory);
+/* @ngInject */
+function NameValidatorFactory(EndpointService, Notifications) {
+ return {
+ validateEnvironmentName,
+ };
+
+ async function validateEnvironmentName(environmentName) {
+ try {
+ const endpoints = await EndpointService.endpoints();
+ const endpointArray = endpoints.value;
+ const nameDuplicated = endpointArray.filter((item) => item.Name === environmentName);
+ return nameDuplicated.length > 0;
+ } catch (err) {
+ Notifications.error('Failure', err, 'Unable to retrieve environment details');
+ }
+ }
+}
diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js
index 1735c66bd..5a095cc2f 100644
--- a/app/portainer/views/auth/authController.js
+++ b/app/portainer/views/auth/authController.js
@@ -124,7 +124,7 @@ class AuthenticationController {
const isAdmin = this.Authentication.isAdmin();
if (endpoints.value.length === 0 && isAdmin) {
- return this.$state.go('portainer.init.endpoint');
+ return this.$state.go('portainer.wizard');
} else {
return this.$state.go('portainer.home');
}
diff --git a/app/portainer/views/init/admin/initAdminController.js b/app/portainer/views/init/admin/initAdminController.js
index 31b0c34bc..9ab139442 100644
--- a/app/portainer/views/init/admin/initAdminController.js
+++ b/app/portainer/views/init/admin/initAdminController.js
@@ -54,7 +54,7 @@ angular.module('portainer.app').controller('InitAdminController', [
})
.then(function success(data) {
if (data.value.length === 0) {
- $state.go('portainer.init.endpoint');
+ $state.go('portainer.wizard');
} else {
$state.go('portainer.home');
}
@@ -71,7 +71,7 @@ angular.module('portainer.app').controller('InitAdminController', [
UserService.administratorExists()
.then(function success(exists) {
if (exists) {
- $state.go('portainer.home');
+ $state.go('portainer.wizard');
}
})
.catch(function error(err) {
diff --git a/app/portainer/views/wizard/index.js b/app/portainer/views/wizard/index.js
new file mode 100644
index 000000000..d343c724a
--- /dev/null
+++ b/app/portainer/views/wizard/index.js
@@ -0,0 +1,7 @@
+import angular from 'angular';
+import controller from './wizard-view.controller.js';
+
+angular.module('portainer.app').component('wizardView', {
+ templateUrl: './wizard-view.html',
+ controller,
+});
diff --git a/app/portainer/views/wizard/wizard-endpoints/index.js b/app/portainer/views/wizard/wizard-endpoints/index.js
new file mode 100644
index 000000000..ea92654c6
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/index.js
@@ -0,0 +1,8 @@
+import angular from 'angular';
+import controller from './wizard-endpoints.controller.js';
+import './wizard-endpoints.css';
+
+angular.module('portainer.app').component('wizardEndpoints', {
+ templateUrl: './wizard-endpoints.html',
+ controller,
+});
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-aci/index.js b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-aci/index.js
new file mode 100644
index 000000000..5b7a5d254
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-aci/index.js
@@ -0,0 +1,11 @@
+import angular from 'angular';
+import controller from './wizard-aci.controller.js';
+
+angular.module('portainer.app').component('wizardAci', {
+ templateUrl: './wizard-aci.html',
+ controller,
+ bindings: {
+ onUpdate: '<',
+ onAnalytics: '<',
+ },
+});
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-aci/wizard-aci.controller.js b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-aci/wizard-aci.controller.js
new file mode 100644
index 000000000..a698d7775
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-aci/wizard-aci.controller.js
@@ -0,0 +1,63 @@
+import { buildOption } from '@/portainer/components/box-selector';
+
+export default class WizardAciController {
+ /* @ngInject */
+ constructor($async, EndpointService, Notifications, NameValidator) {
+ this.$async = $async;
+ this.EndpointService = EndpointService;
+ this.Notifications = Notifications;
+ this.NameValidator = NameValidator;
+ }
+
+ addAciEndpoint() {
+ return this.$async(async () => {
+ const { name, azureApplicationId, azureTenantId, azureAuthenticationKey } = this.formValues;
+ const groupId = 1;
+ const tagIds = [];
+
+ try {
+ this.state.actionInProgress = true;
+ // Check name is duplicated or not
+ let nameUsed = await this.NameValidator.validateEnvironmentName(name);
+ if (nameUsed) {
+ this.Notifications.error('Failure', true, 'This name is been used, please try another one');
+ return;
+ }
+ await this.EndpointService.createAzureEndpoint(name, azureApplicationId, azureTenantId, azureAuthenticationKey, groupId, tagIds);
+ this.Notifications.success('Environment connected', name);
+ this.clearForm();
+ this.onUpdate();
+ this.onAnalytics('aci-api');
+ } catch (err) {
+ this.Notifications.error('Failure', err, 'Unable to connect your environment');
+ } finally {
+ this.state.actionInProgress = false;
+ }
+ });
+ }
+
+ clearForm() {
+ this.formValues = {
+ name: '',
+ azureApplicationId: '',
+ azureTenantId: '',
+ azureAuthenticationKey: '',
+ };
+ }
+
+ $onInit() {
+ return this.$async(async () => {
+ this.state = {
+ actionInProgress: false,
+ endpointType: 'api',
+ availableOptions: [buildOption('API', 'fa fa-bolt', 'API', '', 'api')],
+ };
+ this.formValues = {
+ name: '',
+ azureApplicationId: '',
+ azureTenantId: '',
+ azureAuthenticationKey: '',
+ };
+ });
+ }
+}
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-aci/wizard-aci.html b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-aci/wizard-aci.html
new file mode 100644
index 000000000..0232d4bcb
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-aci/wizard-aci.html
@@ -0,0 +1,67 @@
+
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-docker/index.js b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-docker/index.js
new file mode 100644
index 000000000..3ec63022e
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-docker/index.js
@@ -0,0 +1,11 @@
+import angular from 'angular';
+import controller from './wizard-docker.controller.js';
+
+angular.module('portainer.app').component('wizardDocker', {
+ templateUrl: './wizard-docker.html',
+ controller,
+ bindings: {
+ onUpdate: '<',
+ onAnalytics: '<',
+ },
+});
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-docker/wizard-docker.controller.js b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-docker/wizard-docker.controller.js
new file mode 100644
index 000000000..5d82d6197
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-docker/wizard-docker.controller.js
@@ -0,0 +1,211 @@
+//import { getAgentShortVersion } from 'Portainer/views/endpoints/helpers';
+import { PortainerEndpointCreationTypes } from 'Portainer/models/endpoint/models';
+import { buildOption } from '@/portainer/components/box-selector';
+import { EndpointSecurityFormData } from 'Portainer/components/endpointSecurity/porEndpointSecurityModel';
+
+export default class WizardDockerController {
+ /* @ngInject */
+ constructor($async, EndpointService, StateManager, Notifications, clipboard, $filter, NameValidator) {
+ this.$async = $async;
+ this.EndpointService = EndpointService;
+ this.StateManager = StateManager;
+ this.Notifications = Notifications;
+ this.clipboard = clipboard;
+ this.$filter = $filter;
+ this.NameValidator = NameValidator;
+ }
+
+ copyLinuxCommand() {
+ this.clipboard.copyText(this.command.linuxCommand);
+ $('#linuxCommandNotification').show().fadeOut(2500);
+ }
+
+ copyWinCommand() {
+ this.clipboard.copyText(this.command.winCommand);
+ $('#winCommandNotification').show().fadeOut(2500);
+ }
+
+ copyLinuxSocket() {
+ this.clipboard.copyText(this.command.linuxSocket);
+ $('#linuxSocketNotification').show().fadeOut(2500);
+ }
+
+ copyWinSocket() {
+ this.clipboard.copyText(this.command.winSocket);
+ $('#winSocketNotification').show().fadeOut(2500);
+ }
+
+ onChangeFile(file) {
+ this.formValues.securityFormData = file;
+ }
+
+ // connect docker environment
+ connectEnvironment(type) {
+ return this.$async(async () => {
+ const name = this.formValues.name;
+ const url = this.$filter('stripprotocol')(this.formValues.url);
+ const publicUrl = url.split(':')[0];
+ const overrideUrl = this.formValues.socketPath;
+ const groupId = this.formValues.groupId;
+ const tagIds = this.formValues.tagIds;
+ const securityData = this.formValues.securityFormData;
+ const socketUrl = this.formValues.overrideSocket ? overrideUrl : url;
+
+ var creationType = null;
+
+ if (type === 'agent') {
+ creationType = PortainerEndpointCreationTypes.AgentEnvironment;
+ }
+
+ if (type === 'api') {
+ creationType = PortainerEndpointCreationTypes.LocalDockerEnvironment;
+ }
+
+ // Check name is duplicated or not
+ const nameUsed = await this.NameValidator.validateEnvironmentName(name);
+ if (nameUsed) {
+ this.Notifications.error('Failure', true, 'This name is been used, please try another one');
+ return;
+ }
+ switch (type) {
+ case 'agent':
+ await this.addDockerAgentEndpoint(name, creationType, url, publicUrl, groupId, tagIds);
+ break;
+ case 'api':
+ await this.addDockerApiEndpoint(name, creationType, url, publicUrl, groupId, tagIds, securityData);
+ break;
+ case 'socket':
+ await this.addDockerLocalEndpoint(name, socketUrl, publicUrl, groupId, tagIds);
+ break;
+ }
+ });
+ }
+
+ // Docker Agent Endpoint
+ async addDockerAgentEndpoint(name, creationType, url, publicUrl, groupId, tagIds) {
+ const tsl = true;
+ const tlsSkipVerify = true;
+ const tlsSkipClientVerify = true;
+ const tlsCaFile = null;
+ const tlsCertFile = null;
+ const tlsKeyFile = null;
+
+ await this.addRemoteEndpoint(name, creationType, url, publicUrl, groupId, tagIds, tsl, tlsSkipVerify, tlsSkipClientVerify, tlsCaFile, tlsCertFile, tlsKeyFile);
+ }
+
+ // Docker Api Endpoint
+ async addDockerApiEndpoint(name, creationType, url, publicUrl, groupId, tagIds, securityData) {
+ const tsl = this.formValues.tls;
+ const tlsSkipVerify = this.formValues.skipCertification;
+ const tlsSkipClientVerify = this.formValues.skipCertification;
+ const tlsCaFile = tlsSkipVerify ? null : securityData.TLSCACert;
+ const tlsCertFile = tlsSkipClientVerify ? null : securityData.TLSCert;
+ const tlsKeyFile = tlsSkipClientVerify ? null : securityData.TLSKey;
+
+ await this.addRemoteEndpoint(name, creationType, url, publicUrl, groupId, tagIds, tsl, tlsSkipVerify, tlsSkipClientVerify, tlsCaFile, tlsCertFile, tlsKeyFile);
+ }
+
+ async addDockerLocalEndpoint(name, url, publicUrl, groupId, tagIds) {
+ this.state.actionInProgress = true;
+ try {
+ await this.EndpointService.createLocalEndpoint(name, url, publicUrl, groupId, tagIds);
+ this.Notifications.success('Environment connected', name);
+ this.clearForm();
+ this.onUpdate();
+ } catch (err) {
+ this.Notifications.error('Failure', err, 'Unable to connect your environment');
+ } finally {
+ this.state.actionInProgress = false;
+ }
+ }
+
+ async addRemoteEndpoint(name, creationType, url, publicURL, groupId, tagIds, TLS, tlsSkipVerify, tlsSkipClientVerify, tlsCaFile, tlsCertFile, tlsKeyFile) {
+ this.state.actionInProgress = true;
+ try {
+ await this.EndpointService.createRemoteEndpoint(
+ name,
+ creationType,
+ url,
+ publicURL,
+ groupId,
+ tagIds,
+ TLS,
+ tlsSkipVerify,
+ tlsSkipClientVerify,
+ tlsCaFile,
+ tlsCertFile,
+ tlsKeyFile
+ );
+ this.Notifications.success('Environment connected', name);
+ this.clearForm();
+ this.onUpdate();
+
+ if (creationType === PortainerEndpointCreationTypes.AgentEnvironment) {
+ this.onAnalytics('docker-agent');
+ }
+
+ if (creationType === PortainerEndpointCreationTypes.LocalDockerEnvironment) {
+ this.onAnalytics('docker-api');
+ }
+ } catch (err) {
+ this.Notifications.error('Failure', err, 'Unable to connect your environment');
+ } finally {
+ this.state.actionInProgress = false;
+ }
+ }
+
+ clearForm() {
+ this.formValues = {
+ name: '',
+ url: '',
+ publicURL: '',
+ groupId: 1,
+ tagIds: [],
+ environmentUrl: '',
+ dockerApiurl: '',
+ socketPath: '',
+ overrodeSocket: false,
+ skipCertification: false,
+ tls: false,
+ securityFormData: new EndpointSecurityFormData(),
+ };
+ }
+
+ $onInit() {
+ return this.$async(async () => {
+ this.state = {
+ endpointType: 'agent',
+ ConnectSocket: false,
+ actionInProgress: false,
+ endpoints: [],
+ availableOptions: [
+ buildOption('Agent', 'fa fa-bolt', 'Agent', '', 'agent'),
+ buildOption('API', 'fa fa-cloud', 'API', '', 'api'),
+ buildOption('Socket', 'fab fa-docker', 'Socket', '', 'socket'),
+ ],
+ };
+
+ this.formValues = {
+ name: '',
+ url: '',
+ publicURL: '',
+ groupId: 1,
+ tagIds: [],
+ environmentUrl: '',
+ dockerApiurl: '',
+ socketPath: '',
+ overrideSocket: false,
+ skipCertification: false,
+ tls: false,
+ securityFormData: new EndpointSecurityFormData(),
+ };
+
+ this.command = {
+ linuxCommand: `curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent `,
+ winCommand: `curl -L https://downloads.portainer.io/agent-stack-windows.yml -o agent-stack-windows.yml && docker stack deploy --compose-file=agent-stack-windows.yml portainer-agent `,
+ linuxSocket: `-v "/var/run/docker.sock:/var/run/docker.sock" `,
+ winSocket: `-v \.\pipe\docker_engine:\.\pipe\docker_engine `,
+ };
+ });
+ }
+}
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-docker/wizard-docker.html b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-docker/wizard-docker.html
new file mode 100644
index 000000000..18f1884ca
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-docker/wizard-docker.html
@@ -0,0 +1,172 @@
+
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-kubernetes/index.js b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-kubernetes/index.js
new file mode 100644
index 000000000..86d40a5aa
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-kubernetes/index.js
@@ -0,0 +1,11 @@
+import angular from 'angular';
+import controller from './wizard-kubernetes.controller.js';
+
+angular.module('portainer.app').component('wizardKubernetes', {
+ templateUrl: './wizard-kubernetes.html',
+ controller,
+ bindings: {
+ onUpdate: '<',
+ onAnalytics: '<',
+ },
+});
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-kubernetes/wizard-kubernetes.controller.js b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-kubernetes/wizard-kubernetes.controller.js
new file mode 100644
index 000000000..0971e8b9b
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-kubernetes/wizard-kubernetes.controller.js
@@ -0,0 +1,105 @@
+import { PortainerEndpointCreationTypes } from 'Portainer/models/endpoint/models';
+//import { getAgentShortVersion } from 'Portainer/views/endpoints/helpers';
+import { buildOption } from '@/portainer/components/box-selector';
+
+export default class WizardKubernetesController {
+ /* @ngInject */
+ constructor($async, EndpointService, StateManager, Notifications, $filter, clipboard, NameValidator) {
+ this.$async = $async;
+ this.EndpointService = EndpointService;
+ this.StateManager = StateManager;
+ this.Notifications = Notifications;
+ this.$filter = $filter;
+ this.clipboard = clipboard;
+ this.NameValidator = NameValidator;
+ }
+
+ addKubernetesAgent() {
+ return this.$async(async () => {
+ const name = this.state.formValues.name;
+ const groupId = 1;
+ const tagIds = [];
+ const url = this.$filter('stripprotocol')(this.state.formValues.url);
+ const publicUrl = url.split(':')[0];
+ const creationType = PortainerEndpointCreationTypes.AgentEnvironment;
+ const tls = true;
+ const tlsSkipVerify = true;
+ const tlsSkipClientVerify = true;
+ const tlsCaFile = null;
+ const tlsCertFile = null;
+ const tlsKeyFile = null;
+
+ // Check name is duplicated or not
+ let nameUsed = await this.NameValidator.validateEnvironmentName(name);
+ if (nameUsed) {
+ this.Notifications.error('Failure', true, 'This name is been used, please try another one');
+ return;
+ }
+ await this.addRemoteEndpoint(name, creationType, url, publicUrl, groupId, tagIds, tls, tlsSkipVerify, tlsSkipClientVerify, tlsCaFile, tlsCertFile, tlsKeyFile);
+ });
+ }
+
+ async addRemoteEndpoint(name, creationType, url, publicURL, groupId, tagIds, tls, tlsSkipVerify, tlsSkipClientVerify, tlsCaFile, tlsCertFile, tlsKeyFile) {
+ this.state.actionInProgress = true;
+ try {
+ await this.EndpointService.createRemoteEndpoint(
+ name,
+ creationType,
+ url,
+ publicURL,
+ groupId,
+ tagIds,
+ tls,
+ tlsSkipVerify,
+ tlsSkipClientVerify,
+ tlsCaFile,
+ tlsCertFile,
+ tlsKeyFile
+ );
+ this.Notifications.success('Environment connected', name);
+ this.clearForm();
+ this.onUpdate();
+ this.onAnalytics('kubernetes-agent');
+ } catch (err) {
+ this.Notifications.error('Failure', err, 'Unable to conect your environment');
+ } finally {
+ this.state.actionInProgress = false;
+ }
+ }
+
+ copyLoadBalancer() {
+ this.clipboard.copyText(this.command.loadBalancer);
+ $('#loadBalancerNotification').show().fadeOut(2500);
+ }
+
+ copyNodePort() {
+ this.clipboard.copyText(this.command.nodePort);
+ $('#nodePortNotification').show().fadeOut(2500);
+ }
+
+ clearForm() {
+ this.state.formValues = {
+ name: '',
+ url: '',
+ };
+ }
+
+ $onInit() {
+ return this.$async(async () => {
+ this.state = {
+ endpointType: 'agent',
+ actionInProgress: false,
+ formValues: {
+ name: '',
+ url: '',
+ },
+ availableOptions: [buildOption('Agent', 'fa fa-bolt', 'Agent', '', 'agent')],
+ };
+
+ this.command = {
+ loadBalancer: `curl -L https://downloads.portainer.io/portainer-agent-k8s-lb.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml `,
+ nodePort: `curl -L https://downloads.portainer.io/portainer-agent-k8s-nodeport.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml `,
+ };
+ });
+ }
+}
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-kubernetes/wizard-kubernetes.html b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-kubernetes/wizard-kubernetes.html
new file mode 100644
index 000000000..cd6cac537
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-kubernetes/wizard-kubernetes.html
@@ -0,0 +1,65 @@
+
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-list/index.js b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-list/index.js
new file mode 100644
index 000000000..26c5f21db
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-list/index.js
@@ -0,0 +1,9 @@
+import angular from 'angular';
+import './wizard-endpoint-list.css';
+
+angular.module('portainer.app').component('wizardEndpointList', {
+ templateUrl: './wizard-endpoint-list.html',
+ bindings: {
+ endpointList: '<',
+ },
+});
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-list/wizard-endpoint-list.css b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-list/wizard-endpoint-list.css
new file mode 100644
index 000000000..82be4836e
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-list/wizard-endpoint-list.css
@@ -0,0 +1,41 @@
+.wizard-list-wrapper {
+ display: grid;
+ grid-template-columns: 50px 1fr;
+ grid-template-areas:
+ 'image title'
+ 'image subtitle'
+ 'image type';
+ border: 1px solid rgb(221, 221, 221);
+ border-radius: 5px;
+ margin-bottom: 10px;
+ margin-top: 3px;
+ padding: 10px;
+ box-shadow: 0 3px 10px -2px rgb(161 170 166 / 20%);
+}
+
+.wizard-list-image {
+ grid-area: image;
+ font-size: 35px;
+ color: #337ab7;
+}
+
+.wizard-list-title {
+ grid-column: title;
+ padding: 0px 5px;
+ font-weight: bold;
+ font-size: 14px;
+}
+
+.wizard-list-subtitle {
+ grid-column: subtitle;
+ padding: 0px 5px;
+ font-size: 10px;
+ color: rgb(129, 129, 129);
+}
+
+.wizard-list-type {
+ grid-column: type;
+ padding: 0px 5px;
+ font-size: 10px;
+ color: rgb(129, 129, 129);
+}
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-list/wizard-endpoint-list.html b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-list/wizard-endpoint-list.html
new file mode 100644
index 000000000..2feaa7c74
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoint-list/wizard-endpoint-list.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
{{ endpoint.Name }}
+
URL: {{ endpoint.URL | stripprotocol }}
+
Type: {{ endpoint.Type | endpointtypename }}
+
+
+
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.controller.js b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.controller.js
new file mode 100644
index 000000000..57a164304
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.controller.js
@@ -0,0 +1,196 @@
+export default class WizardEndpointsController {
+ /* @ngInject */
+ constructor($async, $scope, $state, EndpointService, $analytics) {
+ this.$async = $async;
+ this.$scope = $scope;
+ this.$state = $state;
+ this.EndpointService = EndpointService;
+ this.$analytics = $analytics;
+
+ this.updateEndpoint = this.updateEndpoint.bind(this);
+ this.addAnalytics = this.addAnalytics.bind(this);
+ }
+ /**
+ * WIZARD ENDPOINT SECTION
+ */
+
+ async updateEndpoint() {
+ const updateEndpoints = await this.EndpointService.endpoints();
+ this.endpoints = updateEndpoints.value;
+ }
+
+ startWizard() {
+ const options = this.state.options;
+ this.state.selections = options.filter((item) => item.selected === true);
+ this.state.maxStep = this.state.selections.length;
+
+ if (this.state.selections.length !== 0) {
+ this.state.section = this.state.selections[this.state.currentStep].endpoint;
+ this.state.selections[this.state.currentStep].stage = 'active';
+ }
+
+ if (this.state.currentStep === this.state.maxStep - 1) {
+ this.state.nextStep = 'Finish';
+ }
+
+ this.$analytics.eventTrack('endpoint-wizard-endpoint-select', {
+ category: 'portainer',
+ metadata: {
+ environment: this.state.analytics.docker + this.state.analytics.kubernetes + this.state.analytics.aci,
+ },
+ });
+ this.state.currentStep++;
+ }
+
+ previousStep() {
+ this.state.section = this.state.selections[this.state.currentStep - 2].endpoint;
+ this.state.selections[this.state.currentStep - 2].stage = 'active';
+ this.state.selections[this.state.currentStep - 1].stage = '';
+ this.state.nextStep = 'Next Step';
+ this.state.currentStep--;
+ }
+
+ async nextStep() {
+ if (this.state.currentStep >= this.state.maxStep - 1) {
+ this.state.nextStep = 'Finish';
+ }
+ if (this.state.currentStep === this.state.maxStep) {
+ // the Local Endpoint Counter from endpoints array due to including Local Endpoint been added Automatic before Wizard start
+ const endpointsAdded = await this.EndpointService.endpoints();
+ const endpointsArray = endpointsAdded.value;
+ const filter = endpointsArray.filter((item) => item.Type === 1 || item.Type === 5);
+ // NOTICE: This is the temporary fix for excluded docker api endpoint been counted as local endpoint
+ this.state.counter.localEndpoint = filter.length - this.state.counter.dockerApi;
+
+ this.$analytics.eventTrack('endpoint-wizard-environment-add-finish', {
+ category: 'portainer',
+ metadata: {
+ 'docker-agent': this.state.counter.dockerAgent,
+ 'docker-api': this.state.counter.dockerApi,
+ 'kubernetes-agent': this.state.counter.kubernetesAgent,
+ 'aci-api': this.state.counter.aciApi,
+ 'local-endpoint': this.state.counter.localEndpoint,
+ },
+ });
+ this.$state.go('portainer.home');
+ } else {
+ this.state.section = this.state.selections[this.state.currentStep].endpoint;
+ this.state.selections[this.state.currentStep].stage = 'active';
+ this.state.selections[this.state.currentStep - 1].stage = 'completed';
+ this.state.currentStep++;
+ }
+ }
+
+ addAnalytics(endpoint) {
+ switch (endpoint) {
+ case 'docker-agent':
+ this.state.counter.dockerAgent++;
+ break;
+ case 'docker-api':
+ this.state.counter.dockerApi++;
+ break;
+ case 'kubernetes-agent':
+ this.state.counter.kubernetesAgent++;
+ break;
+ case 'aci-api':
+ this.state.counter.aciApi++;
+ break;
+ }
+ }
+
+ endpointSelect(endpoint) {
+ switch (endpoint) {
+ case 'docker':
+ if (this.state.options[0].selected) {
+ this.state.options[0].selected = false;
+ this.state.dockerActive = '';
+ this.state.analytics.docker = '';
+ } else {
+ this.state.options[0].selected = true;
+ this.state.dockerActive = 'wizard-active';
+ this.state.analytics.docker = 'Docker/';
+ }
+ break;
+ case 'kubernetes':
+ if (this.state.options[1].selected) {
+ this.state.options[1].selected = false;
+ this.state.kubernetesActive = '';
+ this.state.analytics.kubernetes = '';
+ } else {
+ this.state.options[1].selected = true;
+ this.state.kubernetesActive = 'wizard-active';
+ this.state.analytics.kubernetes = 'Kubernetes/';
+ }
+ break;
+ case 'aci':
+ if (this.state.options[2].selected) {
+ this.state.options[2].selected = false;
+ this.state.aciActive = '';
+ this.state.analytics.aci = '';
+ } else {
+ this.state.options[2].selected = true;
+ this.state.aciActive = 'wizard-active';
+ this.state.analytics.aci = 'ACI';
+ }
+ break;
+ }
+ const options = this.state.options;
+ this.state.selections = options.filter((item) => item.selected === true);
+ }
+
+ $onInit() {
+ return this.$async(async () => {
+ (this.state = {
+ currentStep: 0,
+ section: '',
+ dockerActive: '',
+ kubernetesActive: '',
+ aciActive: '',
+ maxStep: '',
+ previousStep: 'Previous',
+ nextStep: 'Next Step',
+ selections: [],
+ analytics: {
+ docker: '',
+ kubernetes: '',
+ aci: '',
+ },
+ counter: {
+ dockerAgent: 0,
+ dockerApi: 0,
+ kubernetesAgent: 0,
+ aciApi: 0,
+ localEndpoint: 0,
+ },
+ options: [
+ {
+ endpoint: 'docker',
+ selected: false,
+ stage: '',
+ nameClass: 'docker',
+ icon: 'fab fa-docker',
+ },
+ {
+ endpoint: 'kubernetes',
+ selected: false,
+ stage: '',
+ nameClass: 'kubernetes',
+ icon: 'fas fa-dharmachakra',
+ },
+ {
+ endpoint: 'aci',
+ selected: false,
+ stage: '',
+ nameClass: 'aci',
+ icon: 'fab fa-microsoft',
+ },
+ ],
+ selectOption: '',
+ }),
+ (this.endpoints = []);
+
+ const endpoints = await this.EndpointService.endpoints();
+ this.endpoints = endpoints.value;
+ });
+ }
+}
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.css b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.css
new file mode 100644
index 000000000..8b6a8a56f
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.css
@@ -0,0 +1,155 @@
+.wizard-endpoints {
+ display: block;
+ width: 200px;
+ height: 300px;
+ border: 1px solid rgb(163, 163, 163);
+ border-radius: 5px;
+ float: left;
+ margin-right: 15px;
+ padding: 25px 20px;
+ cursor: pointer;
+ box-shadow: 0 3px 10px -2px rgb(161 170 166 / 60%);
+}
+
+.wizard-endpoints:hover {
+ box-shadow: 0 3px 10px -2px rgb(161 170 166 / 80%);
+ border: 1px solid #3ca4ff;
+ color: #337ab7;
+}
+
+.wizard-active:hover {
+ color: #fff;
+}
+
+.wizard-active {
+ background: #337ab7;
+ color: #fff;
+ border: 1px solid #3ca4ff;
+ box-shadow: 0 3px 10px -2px rgb(161 170 166 / 80%);
+}
+
+.wizard-form-required {
+ color: rgb(255, 24, 24);
+ padding: 0px 5px;
+}
+
+.wizard-form {
+ margin-top: 40px;
+}
+
+.wizard-code {
+ margin-right: 15px;
+}
+
+.wizard-action {
+ margin-top: 20px;
+}
+
+.wizard-connect-button {
+ margin-left: 0px !important;
+ margin-top: 40px;
+}
+
+.wizard-copy-button {
+ color: #444;
+ cursor: pointer;
+}
+
+.wizard-step-action {
+ padding-top: 40px;
+ padding-bottom: 40px;
+ text-align: right;
+ border-top: 1px solid #777;
+}
+
+.next-btn {
+ float: right;
+}
+
+.previous-btn {
+ float: left;
+ margin-left: 0px !important;
+}
+
+.wizard-wrapper {
+ display: grid;
+ grid-template-columns: 1fr 400px;
+ grid-template-areas:
+ 'main sidebar'
+ 'footer sidebar';
+}
+
+.wizard-main {
+ grid-column: main;
+}
+
+.wizard-aside {
+ grid-column: sidebar;
+ margin-right: 15px;
+}
+
+.wizard-footer {
+ grid-column: footer;
+}
+
+.wizard-endpoint-section {
+ padding-right: 10px;
+}
+
+.wizard-main-title {
+ margin-bottom: 10px;
+}
+
+.wizard-env-section {
+ display: block;
+ padding: 10px;
+ border: 1px solid red;
+ width: 80%;
+ margin-left: auto;
+ margin-right: auto;
+ height: 600px;
+ text-align: center;
+}
+
+.wizard-env-icon {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.wizard-content-wrapper {
+ position: relative;
+ left: 50%;
+}
+
+.wizard-content {
+ float: left;
+ position: relative;
+ left: -50%;
+}
+
+.wizard-section {
+ display: grid;
+ justify-content: left;
+ align-content: left;
+ gap: 10px;
+ grid-auto-flow: column;
+ margin-bottom: 20px;
+}
+
+.wizard-section-title {
+ font-size: 32px;
+ margin-top: 30px;
+ margin-bottom: 15px;
+}
+.wizard-setion-subtitle {
+ font-size: 18px;
+}
+
+.wizard-section-action {
+ margin-top: 50px;
+ margin-bottom: 20px;
+}
+
+.no-margin {
+ margin-left: 0px;
+}
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.html b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.html
new file mode 100644
index 000000000..d54eb5bd7
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.html
@@ -0,0 +1,96 @@
+
+
+ Environment Wizard
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Connect to your {{ $ctrl.state.section }} environment
+
+
+
+
+
+
+
+
+
+ {{ $ctrl.state.previousStep }}
+
+
+ {{ $ctrl.state.nextStep }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Select your environment(s)
+
+
+ You can onboard different types of environments, select all that apply.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-stepper/index.js b/app/portainer/views/wizard/wizard-endpoints/wizard-stepper/index.js
new file mode 100644
index 000000000..5e0e78b0b
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-stepper/index.js
@@ -0,0 +1,9 @@
+import angular from 'angular';
+import './wizard-stepper.css';
+
+angular.module('portainer.app').component('wizardStepper', {
+ templateUrl: './wizard-stepper.html',
+ bindings: {
+ endpointSelections: '<',
+ },
+});
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-stepper/wizard-endpoint-type/index.js b/app/portainer/views/wizard/wizard-endpoints/wizard-stepper/wizard-endpoint-type/index.js
new file mode 100644
index 000000000..fb51b1401
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-stepper/wizard-endpoint-type/index.js
@@ -0,0 +1,11 @@
+import angular from 'angular';
+
+angular.module('portainer.app').component('wizardEndpointType', {
+ templateUrl: './wizard-endpoint-type.html',
+ bindings: {
+ endpointTitle: '@',
+ description: '@',
+ icon: '@',
+ active: '<',
+ },
+});
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-stepper/wizard-endpoint-type/wizard-endpoint-type.html b/app/portainer/views/wizard/wizard-endpoints/wizard-stepper/wizard-endpoint-type/wizard-endpoint-type.html
new file mode 100644
index 000000000..3088e3616
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-stepper/wizard-endpoint-type/wizard-endpoint-type.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
{{ $ctrl.endpointTitle }}
+ {{ $ctrl.description }}
+
+
+
+
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-stepper/wizard-stepper.css b/app/portainer/views/wizard/wizard-endpoints/wizard-stepper/wizard-stepper.css
new file mode 100644
index 000000000..2bff2354e
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-stepper/wizard-stepper.css
@@ -0,0 +1,95 @@
+.stepper-wrapper {
+ width: 60%;
+ margin-top: auto;
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 20px;
+ margin-left: 10px;
+}
+.stepper-item {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ flex: 1;
+}
+
+.docker {
+ margin-left: -5px;
+ text-transform: capitalize;
+}
+.kubernetes {
+ margin-left: -20px;
+ text-transform: capitalize;
+}
+.aci {
+ margin-left: 5px;
+ text-transform: uppercase;
+}
+
+.stepper-item::before {
+ position: absolute;
+ content: '';
+ border-bottom: 5px solid rgb(231, 231, 231);
+ width: 100%;
+ top: 20px;
+ left: -100%;
+ z-index: 2;
+}
+
+.stepper-item::after {
+ position: absolute;
+ content: '';
+ border-bottom: 5px solid rgb(231, 231, 231);
+ width: 100%;
+ top: 20px;
+ left: 0;
+ z-index: 2;
+}
+
+.stepper-item .step-counter {
+ position: relative;
+ z-index: 5;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background: rgb(231, 231, 231);
+ margin-bottom: 6px;
+}
+
+.stepper-item.active {
+ font-weight: bold;
+ background: #fff;
+ content: none;
+}
+
+.stepper-item.active .step-counter {
+ background: #337ab7;
+ color: #fff;
+}
+
+.stepper-item.completed .step-counter {
+ background-color: #48b400;
+ color: #fff;
+}
+
+.stepper-item.completed::after {
+ position: absolute;
+ content: '';
+ border-bottom: 5px solid #48b400;
+ width: 100%;
+ top: 20px;
+ left: 0;
+ z-index: 3;
+}
+
+.stepper-item:first-child::before {
+ content: none;
+}
+
+.stepper-item:last-child::after {
+ content: none;
+}
diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-stepper/wizard-stepper.html b/app/portainer/views/wizard/wizard-endpoints/wizard-stepper/wizard-stepper.html
new file mode 100644
index 000000000..741487c9d
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-endpoints/wizard-stepper/wizard-stepper.html
@@ -0,0 +1,6 @@
+
+
+
{{ $index + 1 }}
+
{{ selection.endpoint }}
+
+
diff --git a/app/portainer/views/wizard/wizard-link/index.js b/app/portainer/views/wizard/wizard-link/index.js
new file mode 100644
index 000000000..e0f8cbb8d
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-link/index.js
@@ -0,0 +1,11 @@
+import angular from 'angular';
+import './wizard-link.css';
+
+angular.module('portainer.app').component('wizardLink', {
+ templateUrl: './wizard-link.html',
+ bindings: {
+ linkTitle: '@',
+ description: '@',
+ icon: '<',
+ },
+});
diff --git a/app/portainer/views/wizard/wizard-link/wizard-link.css b/app/portainer/views/wizard/wizard-link/wizard-link.css
new file mode 100644
index 000000000..f62148e0c
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-link/wizard-link.css
@@ -0,0 +1,41 @@
+.wizard-button {
+ display: block;
+ border: 1px solid rgb(163, 163, 163);
+ border-radius: 5px;
+ width: 200px;
+ height: 300px;
+ float: left;
+ margin-right: 30px;
+ cursor: pointer;
+ padding: 25px 20px;
+ box-shadow: 0 3px 10px -2px rgb(161 170 166 / 60%);
+}
+
+.wizard-button:hover {
+ box-shadow: 0 3px 10px -2px rgb(161 170 166 / 80%);
+ border: 1px solid #3ca4ff;
+ color: #337ab7;
+}
+
+.wizard-link {
+ color: #000;
+}
+
+.wizard-title {
+ color: #000;
+}
+
+.wizard-button-subtitle {
+ color: rgb(112, 112, 112);
+ margin-bottom: 30px;
+ text-align: center;
+}
+
+.wizard-button-title {
+ font-size: 12px;
+ margin-bottom: 15px;
+}
+
+.wizard-link-section {
+ margin-top: 15px;
+}
diff --git a/app/portainer/views/wizard/wizard-link/wizard-link.html b/app/portainer/views/wizard/wizard-link/wizard-link.html
new file mode 100644
index 000000000..5f32b07ca
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-link/wizard-link.html
@@ -0,0 +1,7 @@
+
diff --git a/app/portainer/views/wizard/wizard-tls/index.js b/app/portainer/views/wizard/wizard-tls/index.js
new file mode 100644
index 000000000..3f1cf61cd
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-tls/index.js
@@ -0,0 +1,9 @@
+import angular from 'angular';
+
+angular.module('portainer.app').component('wizardTls', {
+ templateUrl: './wizard-tls.html',
+ bindings: {
+ formData: '<',
+ onChange: '<',
+ },
+});
diff --git a/app/portainer/views/wizard/wizard-tls/wizard-tls.html b/app/portainer/views/wizard/wizard-tls/wizard-tls.html
new file mode 100644
index 000000000..32e7e2fd5
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-tls/wizard-tls.html
@@ -0,0 +1,49 @@
+
diff --git a/app/portainer/views/wizard/wizard-view.controller.js b/app/portainer/views/wizard/wizard-view.controller.js
new file mode 100644
index 000000000..f615b69ef
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-view.controller.js
@@ -0,0 +1,87 @@
+import { PortainerEndpointCreationTypes } from 'Portainer/models/endpoint/models';
+
+export default class WizardViewController {
+ /* @ngInject */
+ constructor($async, $state, EndpointService, $analytics) {
+ this.$async = $async;
+ this.$state = $state;
+ this.EndpointService = EndpointService;
+ this.$analytics = $analytics;
+ }
+
+ /**
+ * WIZARD APPLICATION
+ */
+ manageLocalEndpoint() {
+ this.$state.go('portainer.home');
+ }
+
+ addRemoteEndpoint() {
+ this.$state.go('portainer.wizard.endpoints');
+ }
+
+ async createLocalKubernetesEndpoint() {
+ this.state.endpoint.loading = true;
+ try {
+ await this.EndpointService.createLocalKubernetesEndpoint();
+ this.state.endpoint.loading = false;
+ this.state.endpoint.added = true;
+ this.state.endpoint.connected = 'kubernetes';
+ this.state.local.icon = 'fas fa-dharmachakra';
+ } catch (err) {
+ this.state.endpoint.kubernetesError = true;
+ }
+ }
+
+ async createLocalDockerEndpoint() {
+ try {
+ await this.EndpointService.createLocalEndpoint();
+ this.state.endpoint.loading = false;
+ this.state.endpoint.added = true;
+ this.state.endpoint.connected = 'docker';
+ this.state.local.icon = 'fab fa-docker';
+ } finally {
+ this.state.endpoint.loading = false;
+ }
+ }
+
+ $onInit() {
+ return this.$async(async () => {
+ this.state = {
+ local: {
+ icon: '',
+ },
+ remote: {
+ icon: 'fa fa-plug',
+ },
+ endpoint: {
+ kubernetesError: false,
+ connected: '',
+ loading: false,
+ added: false,
+ },
+ };
+
+ const endpoints = await this.EndpointService.endpoints();
+ if (endpoints.totalCount === '0') {
+ await this.createLocalKubernetesEndpoint();
+ if (this.state.endpoint.kubernetesError) {
+ await this.createLocalDockerEndpoint();
+ }
+ } else {
+ const addedLocalEndpoint = endpoints.value[0];
+ if (addedLocalEndpoint.Type === PortainerEndpointCreationTypes.LocalDockerEnvironment) {
+ this.state.endpoint.added = true;
+ this.state.endpoint.connected = 'docker';
+ this.state.local.icon = 'fab fa-docker';
+ }
+
+ if (addedLocalEndpoint.Type === PortainerEndpointCreationTypes.LocalKubernetesEnvironment) {
+ this.state.endpoint.added = true;
+ this.state.endpoint.connected = 'kubernetes';
+ this.state.local.icon = 'fas fa-dharmachakra';
+ }
+ }
+ });
+ }
+}
diff --git a/app/portainer/views/wizard/wizard-view.html b/app/portainer/views/wizard/wizard-view.html
new file mode 100644
index 000000000..db8c880b6
--- /dev/null
+++ b/app/portainer/views/wizard/wizard-view.html
@@ -0,0 +1,52 @@
+
+
+ Endpoint Wizard
+
+
+
+
+
+
+
+
+
+ Welcome to Portainer
+
+
+
+ We have connected your local environment of {{ $ctrl.state.endpoint.connected }} to Portainer.
+
+ We could not connect your local environment to Portainer.
+ Please ensure your environment is correctly exposed. For help with installation vist
+ https://documentation.portainer.io/quickstart
+
+
+ Get started below with your local portainer or connect more container environments.
+
+
+
+
+
+
+
+
+
+