feat(wizard):first UX experience for adding environment EE-1089 (#5581)

* first UX experience for adding environment EE-1089
pull/5605/head
Richard Wei 2021-09-10 14:25:49 +12:00 committed by GitHub
parent 2a60b8fcdf
commit d8b88d1004
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1676 additions and 3 deletions

View File

@ -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);

View File

@ -6,6 +6,9 @@
</div>
<div class="actionBar" ng-if="$ctrl.showSnapshotAction">
<div style="margin-bottom: 10px;" ng-if="$ctrl.endpoints.length"
><i class="fa fa-exclamation-circle blue-icon" style="margin-right: 5px;"></i>Click on an environment to manage</div
>
<button type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.snapshotAction()" data-cy="home-refreshEndpointsButton">
<i class="fa fa-sync space-right" aria-hidden="true"></i>Refresh
</button>

View File

@ -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');
}
}
}

View File

@ -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');
}

View File

@ -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) {

View File

@ -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,
});

View File

@ -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,
});

View File

@ -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: '<',
},
});

View File

@ -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: '',
};
});
}
}

View File

@ -0,0 +1,67 @@
<form class="form-horizontal" name="aciWizardForm">
<box-selector radio-name="ACI" ng-model="$ctrl.state.endpointType" options="$ctrl.state.availableOptions"></box-selector>
<!-- docker form section-->
<div class="form-group wizard-form">
<label for="acir_name" class="col-sm-3 col-lg-2 control-label text-left">Name<span class="wizard-form-required">*</span></label>
<div class="col-sm-9 col-lg-10" style="margin-bottom: 15px;">
<input type="text" class="form-control" name="aci_name" ng-model="$ctrl.formValues.name" placeholder="e.g. docker-prod01 / kubernetes-cluster01" required auto-focus />
</div>
</div>
<div class="form-group">
<label for="azure_credential_appid" class="col-sm-3 col-lg-2 control-label text-left">Application ID:<span class="wizard-form-required">*</span></label>
<div class="col-sm-9 col-lg-10" style="margin-bottom: 15px;">
<input
type="text"
class="form-control"
name="azure_credential_appid"
ng-model="$ctrl.formValues.azureApplicationId"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
required
/>
</div>
</div>
<div class="form-group">
<label for="azure_credential_tenantid" class="col-sm-3 col-lg-2 control-label text-left">Tenant ID:<span class="wizard-form-required">*</span></label>
<div class="col-sm-9 col-lg-10" style="margin-bottom: 15px;">
<input
type="text"
class="form-control"
name="azure_credential_tenantid"
ng-model="$ctrl.formValues.azureTenantId"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
required
/>
</div>
</div>
<div class="form-group">
<label for="azure_credential_authkey" class="col-sm-3 col-lg-2 control-label text-left">Authentication key<span class="wizard-form-required">*</span></label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
name="azure_credential_authkey"
ng-model="$ctrl.formValues.azureAuthenticationKey"
placeholder="cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk="
required
/>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<button
type="submit"
class="btn btn-primary btn-sm wizard-connect-button"
ng-disabled="!$ctrl.formValues.name || !$ctrl.formValues.azureApplicationId || !$ctrl.formValues.azureTenantId || !$ctrl.formValues.azureAuthenticationKey || $ctrl.state.actionInProgress"
ng-click="$ctrl.addAciEndpoint()"
button-spinner="$ctrl.state.actionInProgress"
>
<span ng-hide="$ctrl.state.actionInProgress"><i class="fa fa-plug" style="margin-right: 5px;"></i> Connect </span>
<span ng-show="$ctrl.state.actionInProgress">Connecting environment...</span>
</button>
</div>
</div>
</form>

View File

@ -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: '<',
},
});

View File

@ -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 `,
};
});
}
}

View File

@ -0,0 +1,172 @@
<form class="form-horizontal" name="dockerWizardForm">
<!-- docker tab selection -->
<box-selector radio-name="Docker" ng-click="$ctrl.clearForm()" ng-model="$ctrl.state.endpointType" options="$ctrl.state.availableOptions"></box-selector>
<!-- docker tab selection -->
<div style="padding-left: 10px;">
<div class="form-group">
<div ng-if="$ctrl.state.endpointType === 'agent'" class="wizard-code">
<uib-tabset>
<uib-tab index="0" heading="Linux">
<code style="display: block; white-space: pre-wrap; padding: 16px 10px;"
><h6 style="color: #000;">CLI script for installing agent on your Linux environment with Docker Swarm</h6>{{ $ctrl.command.linuxCommand
}}<i class="fas fa-copy wizard-copy-button" ng-click="$ctrl.copyLinuxCommand()"></i
><i id="linuxCommandNotification" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i
></code>
</uib-tab>
<uib-tab index="1" heading="Windows">
<code style="display: block; white-space: pre-wrap; padding: 16px 10px;"
><h6 style="color: #000;">CLI script for installing agent on your Windows environment with Docker Swarm</h6>{{ $ctrl.command.winCommand
}}<i class="fas fa-copy wizard-copy-button" ng-click="$ctrl.copyWinCommand()"></i
><i id="winCommandNotification" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i
></code>
</uib-tab>
</uib-tabset>
</div>
<div ng-if="$ctrl.state.endpointType === 'api' || $ctrl.state.endpointType === 'socket'" class="wizard-code">
<uib-tabset active="state.deploymentTab">
<uib-tab index="0" heading="Linux">
<code style="display: block; white-space: pre-wrap; padding: 16px 10px;"
><h6 style="color: #000;">When using the socket, ensure that you have started the Portainer container with the following Docker flag on Linux</h6
>{{ $ctrl.command.linuxSocket }}<i class="fas fa-copy wizard-copy-button" ng-click="$ctrl.copyLinuxSocket()"></i
><i id="linuxSocketNotification" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i
></code>
</uib-tab>
<uib-tab index="1" heading="Windows">
<code style="display: block; white-space: pre-wrap; padding: 16px 10px;"
><h6 style="color: #000;">When using the socket, ensure that you have started the Portainer container with the following Docker flag on Windows</h6
>{{ $ctrl.command.winSocket }}<i class="fas fa-copy wizard-copy-button" ng-click="$ctrl.copyWinSocket()"></i
><i id="winSocketNotification" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i
></code>
</uib-tab>
</uib-tabset>
</div>
</div>
</div>
<!-- docker form section-->
<div class="form-group wizard-form">
<label for="endpoint_name" class="col-sm-3 col-lg-2 control-label text-left">Name<span class="wizard-form-required">*</span></label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="endpoint_name" ng-model="$ctrl.formValues.name" placeholder="e.g. docker-prod01 / kubernetes-cluster01" auto-focus />
</div>
</div>
<div ng-if="$ctrl.state.endpointType === 'agent'" class="form-group">
<label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left"> Environments URL<span class="wizard-form-required">*</span> </label>
<div class="col-sm-9 col-lg-10" style="margin-bottom: 15px;">
<input
ng-if="$ctrl.state.endpointType === 'agent'"
type="text"
class="form-control"
name="endpoint_url"
ng-model="$ctrl.formValues.url"
placeholder="e.g. 10.0.0.10:9001 or tasks.portainer_agent:9001"
/>
</div>
</div>
<div ng-if="$ctrl.state.endpointType === 'api'">
<div class="form-group">
<label for="dockerapi_url" class="col-sm-3 col-lg-2 control-label text-left"> Docker API URL<span class="wizard-form-required">*</span> </label>
<div class="col-sm-9 col-lg-10" style="margin-bottom: 15px;">
<input
ng-if="$ctrl.state.endpointType === 'api'"
type="text"
class="form-control"
name="dockerapi_url"
ng-model="$ctrl.formValues.url"
placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375"
/>
</div>
</div>
<div class="form-group" style="padding-left: 15px; width: 15%;">
<por-switch-field ng-model="$ctrl.formValues.tls" name="connect_socket" label="TLS" label-class="col-sm-12 col-lg-4"></por-switch-field>
</div>
<div class="form-group" style="padding-left: 15px; width: 40%;">
<por-switch-field
ng-if="$ctrl.formValues.tls"
ng-model="$ctrl.formValues.skipCertification"
name="skip_certification"
label="Skip Certification Verification"
label-class="col-sm-12 col-lg-4"
></por-switch-field>
</div>
<div>
<wizard-tls ng-if="!$ctrl.formValues.skipCertification && $ctrl.formValues.tls" form-data="$ctrl.formValues.securityFormData" onChange="($ctrl.onChangeFile)"></wizard-tls>
</div>
</div>
<div ng-if="$ctrl.state.endpointType === 'socket'" class="form-group" style="padding-left: 15px;">
<div class="form-group" style="padding-left: 15px;">
<label for="override_socket" class="col-sm_12 control-label text-left">
Override default socket path
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="$ctrl.formValues.overrideSocket" /><i></i></label>
</div>
<div ng-if="$ctrl.formValues.overrideSocket">
<div class="form-group">
<label for="socket_path" class="col-sm-3 col-lg-2 control-label text-left">
Socket path
<portainer-tooltip position="bottom" message="Path to the Docker socket. Remember to bind-mount the socket, see the important notice above for more information.">
</portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
name="socket_path"
ng-model="$ctrl.formValues.socketPath"
placeholder="e.g. /var/run/docker.sock (on Linux) or //./pipe/docker_engine (on Windows)"
/>
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<button
ng-if="$ctrl.state.endpointType === 'agent'"
type="submit"
class="btn btn-primary btn-sm wizard-connect-button"
ng-disabled="!$ctrl.formValues.name || !$ctrl.formValues.url || $ctrl.state.actionInProgress"
ng-click="$ctrl.connectEnvironment('agent')"
button-spinner="$ctrl.state.actionInProgress"
>
<span ng-hide="$ctrl.state.actionInProgress"><i class="fa fa-plug" style="margin-right: 5px;"></i>Connect </span>
<span ng-show="$ctrl.state.actionInProgress">Connecting environment...</span>
</button>
<button
ng-if="$ctrl.state.endpointType === 'api'"
type="submit"
class="btn btn-primary btn-sm wizard-connect-button"
ng-disabled="!$ctrl.formValues.name || !$ctrl.formValues.url || $ctrl.state.actionInProgress"
ng-click="$ctrl.connectEnvironment('api')"
button-spinner="$ctrl.state.actionInProgress"
>
<span ng-hide="$ctrl.state.actionInProgress"><i class="fa fa-plug" style="margin-right: 5px;"></i>Connect </span>
<span ng-show="$ctrl.state.actionInProgress">Connecting environment...</span>
</button>
<button
ng-if="$ctrl.state.endpointType === 'socket'"
type="submit"
class="btn btn-primary btn-sm wizard-connect-button"
ng-disabled="!$ctrl.formValues.name || $ctrl.state.actionInProgress"
ng-click="$ctrl.connectEnvironment('socket')"
button-spinner="$ctrl.state.actionInProgress"
>
<span ng-hide="$ctrl.state.actionInProgress"><i class="fa fa-plug" style="margin-right: 5px;"></i>Connect </span>
<span ng-show="$ctrl.state.actionInProgress">Connecting environment...</span>
</button>
</div>
</div>
</form>

View File

@ -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: '<',
},
});

View File

@ -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 `,
};
});
}
}

View File

@ -0,0 +1,65 @@
<form class="form-horizontal" name="kubernetesWizardForm">
<box-selector radio-name="Kubernetes" ng-model="$ctrl.state.endpointType" options="$ctrl.state.availableOptions"></box-selector>
<!-- docker tab selection -->
<div style="padding-left: 10px;">
<div class="form-group">
<div class="wizard-code">
<uib-tabset active="state.deploymentTab">
<uib-tab index="0" heading="Kubernetes via load balancer">
<code style="display: block; white-space: pre-wrap; padding: 16px 10px;"
><h6 style="color: #000;">CLI script for installing agent on your endpoint</h6>{{ $ctrl.command.loadBalancer
}}<i class="fas fa-copy wizard-copy-button" ng-click="$ctrl.copyLoadBalancer()"></i
><i id="loadBalancerNotification" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i
></code>
</uib-tab>
<uib-tab index="1" heading="Kubernetes via node port">
<code style="display: block; white-space: pre-wrap; padding: 16px 10px;"
><h6 style="color: #000;">CLI script for installing agent on your endpoint</h6>{{ $ctrl.command.nodePort
}}<i class="fas fa-copy wizard-copy-button" ng-click="$ctrl.copyNodePort()"></i
><i id="nodePortNotification" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i
></code>
</uib-tab>
</uib-tabset>
</div>
</div>
</div>
<div class="form-group wizard-form">
<label for="endpoint_name" class="col-sm-3 col-lg-2 control-label text-left">Name<span class="wizard-form-required">*</span></label>
<div class="col-sm-9 col-lg-10" style="margin-bottom: 15px;">
<input
type="text"
class="form-control"
name="endpoint_name"
ng-model="$ctrl.state.formValues.name"
placeholder="e.g. docker-prod01 / kubernetes-cluster01"
required
auto-focus
/>
</div>
</div>
<div class="form-group">
<label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left"> Environments URL<span class="wizard-form-required">*</span> </label>
<div class="col-sm-9 col-lg-10" style="margin-bottom: 15px;">
<input type="text" class="form-control" name="endpoint_url" ng-model="$ctrl.state.formValues.url" placeholder="e.g. 10.0.0.10:9001 or tasks.portainer_agent:9001" required />
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<button
type="submit"
class="btn btn-primary btn-sm wizard-connect-button"
ng-disabled="!$ctrl.state.formValues.name || !$ctrl.state.formValues.url || $ctrl.state.actionInProgress"
ng-click="$ctrl.addKubernetesAgent()"
button-spinner="$ctrl.state.actionInProgress"
>
<span ng-hide="$ctrl.state.actionInProgress"><i class="fa fa-plug" style="margin-right: 5px;"></i> Connect </span>
<span ng-show="$ctrl.state.actionInProgress">Connecting environment...</span>
</button>
</div>
</div>
</form>

View File

@ -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: '<',
},
});

View File

@ -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);
}

View File

@ -0,0 +1,11 @@
<rd-widget>
<rd-widget-header icon="fa-plug" title-text="Connected Environments"> </rd-widget-header>
<rd-widget-body>
<div class="wizard-list-wrapper" ng-repeat="endpoint in $ctrl.endpointList">
<div class="wizard-list-image"><i ng-class="endpoint.Type | endpointtypeicon" aria-hidden="true" style="margin-right: 2px;"></i></div>
<div class="wizard-list-title">{{ endpoint.Name }}</div>
<div class="wizard-list-subtitle">URL: {{ endpoint.URL | stripprotocol }}</div>
<div class="wizard-list-type">Type: {{ endpoint.Type | endpointtypename }}</div>
</div>
</rd-widget-body>
</rd-widget>

View File

@ -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;
});
}
}

View File

@ -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;
}

View File

@ -0,0 +1,96 @@
<rd-header>
<rd-header-title title-text="Quick Setup"></rd-header-title>
<rd-header-content>Environment Wizard</rd-header-content>
</rd-header>
<div class="wizard-wrapper" ng-if="$ctrl.state.currentStep !== 0">
<div class="wizard-main">
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-magic" title-text="Environment Wizard"> </rd-widget-header>
<rd-widget-body>
<!-- Stepper -->
<wizard-stepper endpoint-selections="$ctrl.state.selections"></wizard-stepper>
<!-- Stepper -->
<div class="col-sm-12 form-section-title wizard-main-title"> Connect to your {{ $ctrl.state.section }} environment </div>
<div ng-switch="$ctrl.state.section" class="wizard-endpoint-section">
<wizard-docker ng-switch-when="docker" on-update="($ctrl.updateEndpoint)" on-analytics="($ctrl.addAnalytics)"></wizard-docker>
<wizard-aci ng-switch-when="aci" on-update="($ctrl.updateEndpoint)" on-analytics="($ctrl.addAnalytics)"></wizard-aci>
<wizard-kubernetes ng-switch-when="kubernetes" on-update="($ctrl.updateEndpoint)" on-analytics="($ctrl.addAnalytics)"></wizard-kubernetes>
</div>
<div class="wizard-step-action">
<button
ng-click="$ctrl.previousStep()"
ng-show="$ctrl.state.currentStep !== 0"
type="submit"
class="btn btn-primary btn-sm previous-btn"
ng-disabled="$ctrl.state.currentStep === 1"
>
<i class="fas fa-arrow-left space-right"></i>{{ $ctrl.state.previousStep }}
</button>
<button ng-click="$ctrl.nextStep()" ng-show="$ctrl.state.currentStep !== 0" type="submit" class="btn btn-primary btn-sm next-btn">
{{ $ctrl.state.nextStep }} <i class="fas fa-arrow-right space-left"></i
></button>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
</div>
<div class="wizard-aside" ng-if="$ctrl.state.currentStep !== 0">
<wizard-endpoint-list endpoint-list="$ctrl.endpoints"></wizard-endpoint-list>
</div>
</div>
<div class="row" ng-if="$ctrl.state.currentStep === 0">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-magic" title-text="Environment Wizard"> </rd-widget-header>
<rd-widget-body>
<div class="row">
<div class="col-sm-12 form-section-title">
Select your environment(s)
</div>
<div>
<span class="text-muted small">You can onboard different types of environments, select all that apply.</span>
</div>
<div class="wizard-section">
<wizard-endpoint-type
endpoint-title="Docker"
description="Connect to Docker Standalone / Swarm via URL/IP, API or Socket"
icon="fab fa-docker"
active="$ctrl.state.dockerActive"
ng-click="$ctrl.endpointSelect('docker')"
></wizard-endpoint-type>
<wizard-endpoint-type
endpoint-title="Kubernetes"
description="Connect to a kubernetes environment via URL/IP"
icon="fas fa-dharmachakra"
active="$ctrl.state.kubernetesActive"
ng-click="$ctrl.endpointSelect('kubernetes')"
></wizard-endpoint-type>
<wizard-endpoint-type
endpoint-title="ACI"
description="Connect to ACI environment via API"
icon="fab fa-microsoft"
active="$ctrl.state.aciActive"
ng-click="$ctrl.endpointSelect('aci')"
></wizard-endpoint-type>
</div>
<div class="wizard-section">
<div class="wizard-section-action">
<button ng-click="$ctrl.startWizard()" ng-disabled="$ctrl.state.selections.length === 0" type="submit" class="btn btn-primary btn-sm no-margin">Start Wizard</button>
</div>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -0,0 +1,9 @@
import angular from 'angular';
import './wizard-stepper.css';
angular.module('portainer.app').component('wizardStepper', {
templateUrl: './wizard-stepper.html',
bindings: {
endpointSelections: '<',
},
});

View File

@ -0,0 +1,11 @@
import angular from 'angular';
angular.module('portainer.app').component('wizardEndpointType', {
templateUrl: './wizard-endpoint-type.html',
bindings: {
endpointTitle: '@',
description: '@',
icon: '@',
active: '<',
},
});

View File

@ -0,0 +1,12 @@
<div>
<div ng-click="$ctrl.endpointSelect('docker')">
<div class="wizard-endpoints {{ $ctrl.active }}">
<div style="text-align: center; padding: 10px;"><i class="{{ $ctrl.icon }}" style="font-size: 80px;"></i></div>
<div style="margin-top: 15px; text-align: center;">
<h3>{{ $ctrl.endpointTitle }}</h3>
<h5>{{ $ctrl.description }}</h5>
</div>
</div>
</div>
</div>

View File

@ -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;
}

View File

@ -0,0 +1,6 @@
<div class="stepper-wrapper">
<div ng-repeat="selection in $ctrl.endpointSelections" class="stepper-item {{ selection.stage }} ">
<div class="step-counter">{{ $index + 1 }}</div>
<div class="step-name {{ selection.nameClass }}">{{ selection.endpoint }}</div>
</div>
</div>

View File

@ -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: '<',
},
});

View File

@ -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;
}

View File

@ -0,0 +1,7 @@
<div class="wizard-button">
<div style="text-align: center; padding: 10px;"><i class="{{ $ctrl.icon }}" style="font-size: 80px;"></i></div>
<div style="margin-top: 15px; text-align: center;">
<h3>{{ $ctrl.linkTitle }}</h3>
<h5>{{ $ctrl.description }}</h5>
</div>
</div>

View File

@ -0,0 +1,9 @@
import angular from 'angular';
angular.module('portainer.app').component('wizardTls', {
templateUrl: './wizard-tls.html',
bindings: {
formData: '<',
onChange: '<',
},
});

View File

@ -0,0 +1,49 @@
<div>
<div>
<!-- tls-file-ca -->
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">TLS CA certificate</label>
<div class="col-sm-9 col-lg-10">
<button type="button" class="btn btn-sm btn-primary" ngf-select="$ctrl.onChange($file)" ng-model="$ctrl.formData.TLSCACert">Select file</button>
<span class="space-left">
{{ $ctrl.formData.TLSCACert.name }}
<i class="fa fa-check green-icon" ng-if="$ctrl.formData.TLSCACert" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!$ctrl.formData.TLSCACert" aria-hidden="true"></i>
</span>
</div>
</div>
<!-- !tls-file-ca -->
<!-- tls-files-cert-key -->
<div>
<!-- tls-file-cert -->
<div class="form-group">
<label for="tls_cert" class="col-sm-3 col-lg-2 control-label text-left">TLS certificate</label>
<div class="col-sm-9 col-lg-10">
<button type="button" class="btn btn-sm btn-primary" ngf-select="$ctrl.onChange($file)" ng-model="$ctrl.formData.TLSCert">Select file</button>
<span class="space-left">
{{ $ctrl.formData.TLSCert.name }}
<i class="fa fa-check green-icon" ng-if="$ctrl.formData.TLSCert" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!$ctrl.formData.TLSCert" aria-hidden="true"></i>
</span>
</div>
</div>
<!-- !tls-file-cert -->
<!-- tls-file-key -->
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">TLS key</label>
<div class="col-sm-9 col-lg-10">
<button type="button" class="btn btn-sm btn-primary" ngf-select="$ctrl.onChange($file)" ng-model="$ctrl.formData.TLSKey">Select file</button>
<span class="space-left">
{{ $ctrl.formData.TLSKey.name }}
<i class="fa fa-check green-icon" ng-if="$ctrl.formData.TLSKey" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!$ctrl.formData.TLSKey" aria-hidden="true"></i>
</span>
</div>
</div>
<!-- !tls-file-key -->
</div>
<!-- tls-files-cert-key -->
</div>
<!-- !tls-file-upload -->
</div>

View File

@ -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';
}
}
});
}
}

View File

@ -0,0 +1,52 @@
<rd-header>
<rd-header-title title-text="Quick Setup"></rd-header-title>
<rd-header-content>Endpoint Wizard</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-magic" title-text="Environment Wizard"> </rd-widget-header>
<rd-widget-body>
<div class="row">
<div class="col-sm-12 form-section-title">
Welcome to Portainer
</div>
<div>
<span class="text-muted small" ng-show="$ctrl.state.endpoint.added">
We have connected your local environment of {{ $ctrl.state.endpoint.connected }} to Portainer. <br
/></span>
<span class="text-muted small" ng-show="!$ctrl.state.endpoint.loading && !$ctrl.state.endpoint.added">
We could not connect your local environment to Portainer. <br />
Please ensure your environment is correctly exposed. For help with installation vist
<a href="https://documentation.portainer.io/quickstart/">https://documentation.portainer.io/quickstart</a><br />
</span>
<span class="text-muted small">
Get started below with your local portainer or connect more container environments.
</span>
</div>
<wizard-link
ng-show="$ctrl.state.endpoint.added"
icon="$ctrl.state.local.icon"
title="Get Started"
link-title="Get Started"
description="Proceed using the local environment which Portainer is running in"
ui-sref="portainer.home"
analytics-on
analytics-category="portainer"
analytics-event="endpoint-wizard-endpoint-select"
analytics-properties="{ metadata: { environment: 'Get-started-local-environment' }}"
></wizard-link>
<wizard-link
title="Add Environments"
link-title="Add Environments"
icon="$ctrl.state.remote.icon"
description="Connect to other environments"
ui-sref="portainer.wizard.endpoints"
></wizard-link>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>