mirror of
https://github.com/portainer/portainer.git
synced 2025-08-07 23:05:26 +02:00
refactor(custom-templates): migrate list view to react [EE-2256] (#11611)
Some checks failed
ci / build_images (map[arch:amd64 platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
ci / build_images (map[arch:arm platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:s390x platform:linux version:]) (push) Has been cancelled
/ triage (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Test / test-client (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:linux]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
Test / test-server (map[arch:arm64 platform:linux]) (push) Has been cancelled
ci / build_manifests (push) Has been cancelled
Some checks failed
ci / build_images (map[arch:amd64 platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
ci / build_images (map[arch:arm platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:s390x platform:linux version:]) (push) Has been cancelled
/ triage (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Test / test-client (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:linux]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
Test / test-server (map[arch:arm64 platform:linux]) (push) Has been cancelled
ci / build_manifests (push) Has been cancelled
This commit is contained in:
parent
5c6c66f010
commit
94c91035a7
28 changed files with 200 additions and 617 deletions
|
@ -4,16 +4,6 @@ import { r2a } from '@/react-tools/react2angular';
|
|||
import { CustomTemplatesVariablesDefinitionField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
||||
import { CustomTemplatesVariablesField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
|
||||
import { withControlledInput } from '@/react-tools/withControlledInput';
|
||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import {
|
||||
CommonFields,
|
||||
validation as commonFieldsValidation,
|
||||
} from '@/react/portainer/custom-templates/components/CommonFields';
|
||||
import { PlatformField } from '@/react/portainer/custom-templates/components/PlatformSelector';
|
||||
import { TemplateTypeSelector } from '@/react/portainer/custom-templates/components/TemplateTypeSelector';
|
||||
import { withFormValidation } from '@/react-tools/withFormValidation';
|
||||
import { CustomTemplatesList } from '@/react/portainer/templates/custom-templates/ListView/CustomTemplatesList';
|
||||
|
||||
import { VariablesFieldAngular } from './variables-field';
|
||||
|
||||
|
@ -37,33 +27,6 @@ export const ngModule = angular
|
|||
'errors',
|
||||
'isVariablesNamesFromParent',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'customTemplatesList',
|
||||
r2a(withUIRouter(withCurrentUser(CustomTemplatesList)), [
|
||||
'onDelete',
|
||||
'onSelect',
|
||||
'templates',
|
||||
'selectedId',
|
||||
'templateLinkParams',
|
||||
'storageKey',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'customTemplatesPlatformSelector',
|
||||
r2a(PlatformField, ['onChange', 'value'])
|
||||
)
|
||||
.component(
|
||||
'customTemplatesTypeSelector',
|
||||
r2a(TemplateTypeSelector, ['onChange', 'value'])
|
||||
);
|
||||
|
||||
withFormValidation(
|
||||
ngModule,
|
||||
withControlledInput(CommonFields, { values: 'onChange' }),
|
||||
'customTemplatesCommonFields',
|
||||
[],
|
||||
commonFieldsValidation
|
||||
);
|
||||
|
||||
export const customTemplatesModule = ngModule.name;
|
||||
|
|
|
@ -6,6 +6,7 @@ import { withUIRouter } from '@/react-tools/withUIRouter';
|
|||
import { CreateView } from '@/react/portainer/templates/custom-templates/CreateView';
|
||||
import { EditView } from '@/react/portainer/templates/custom-templates/EditView';
|
||||
import { AppTemplatesView } from '@/react/portainer/templates/app-templates/AppTemplatesView';
|
||||
import { ListView } from '@/react/portainer/templates/custom-templates/ListView/ListView';
|
||||
|
||||
export const templatesModule = angular
|
||||
.module('portainer.app.react.views.templates', [])
|
||||
|
@ -13,6 +14,10 @@ export const templatesModule = angular
|
|||
'appTemplatesView',
|
||||
r2a(withCurrentUser(withUIRouter(AppTemplatesView)), [])
|
||||
)
|
||||
.component(
|
||||
'customTemplatesView',
|
||||
r2a(withCurrentUser(withUIRouter(ListView)), [])
|
||||
)
|
||||
.component(
|
||||
'createCustomTemplatesView',
|
||||
r2a(withCurrentUser(withUIRouter(CreateView)), [])
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
<page-header title="'Custom Templates'" breadcrumbs="['Custom Templates']" reload="true"> </page-header>
|
||||
|
||||
<stack-from-custom-template-form-widget
|
||||
ng-if="$ctrl.state.selectedTemplate"
|
||||
template="$ctrl.state.selectedTemplate"
|
||||
unselect="$ctrl.unselectTemplate"
|
||||
></stack-from-custom-template-form-widget>
|
||||
|
||||
<custom-templates-list
|
||||
templates="$ctrl.templates"
|
||||
on-select="($ctrl.selectTemplate)"
|
||||
on-delete="($ctrl.confirmDelete)"
|
||||
selected-id="$ctrl.state.selectedTemplate.Id"
|
||||
storage-key="'docker-custom-templates'"
|
||||
></custom-templates-list>
|
|
@ -1,305 +0,0 @@
|
|||
import _ from 'lodash-es';
|
||||
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { isTemplateVariablesEnabled, renderTemplate } from '@/react/portainer/custom-templates/components/utils';
|
||||
import { confirmDelete } from '@@/modals/confirm';
|
||||
import { getVariablesFieldDefaultValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
|
||||
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/react/portainer/custom-templates/components/CommonFields';
|
||||
|
||||
class CustomTemplatesViewController {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$anchorScroll,
|
||||
$async,
|
||||
$rootScope,
|
||||
$state,
|
||||
Authentication,
|
||||
CustomTemplateService,
|
||||
FormValidator,
|
||||
NetworkService,
|
||||
Notifications,
|
||||
ResourceControlService,
|
||||
StackService,
|
||||
StateManager
|
||||
) {
|
||||
this.$anchorScroll = $anchorScroll;
|
||||
this.$async = $async;
|
||||
this.$rootScope = $rootScope;
|
||||
this.$state = $state;
|
||||
this.Authentication = Authentication;
|
||||
this.CustomTemplateService = CustomTemplateService;
|
||||
this.FormValidator = FormValidator;
|
||||
this.NetworkService = NetworkService;
|
||||
this.Notifications = Notifications;
|
||||
this.ResourceControlService = ResourceControlService;
|
||||
this.StateManager = StateManager;
|
||||
this.StackService = StackService;
|
||||
|
||||
this.isTemplateVariablesEnabled = isTemplateVariablesEnabled;
|
||||
|
||||
this.DOCKER_STANDALONE = 'DOCKER_STANDALONE';
|
||||
this.DOCKER_SWARM_MODE = 'DOCKER_SWARM_MODE';
|
||||
|
||||
this.state = {
|
||||
selectedTemplate: null,
|
||||
showAdvancedOptions: false,
|
||||
formValidationError: '',
|
||||
actionInProgress: false,
|
||||
deployable: false,
|
||||
templateNameRegex: TEMPLATE_NAME_VALIDATION_REGEX,
|
||||
templateContent: '',
|
||||
templateLoadFailed: false,
|
||||
};
|
||||
|
||||
this.currentUser = {
|
||||
isAdmin: false,
|
||||
id: null,
|
||||
};
|
||||
|
||||
this.formValues = {
|
||||
network: '',
|
||||
name: '',
|
||||
fileContent: '',
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
variables: [],
|
||||
};
|
||||
|
||||
this.getTemplates = this.getTemplates.bind(this);
|
||||
this.getTemplatesAsync = this.getTemplatesAsync.bind(this);
|
||||
this.removeTemplates = this.removeTemplates.bind(this);
|
||||
this.removeTemplatesAsync = this.removeTemplatesAsync.bind(this);
|
||||
this.validateForm = this.validateForm.bind(this);
|
||||
this.createStack = this.createStack.bind(this);
|
||||
this.createStackAsync = this.createStackAsync.bind(this);
|
||||
this.selectTemplate = this.selectTemplate.bind(this);
|
||||
this.selectTemplateAsync = this.selectTemplateAsync.bind(this);
|
||||
this.unselectTemplate = this.unselectTemplate.bind(this);
|
||||
this.unselectTemplateAsync = this.unselectTemplateAsync.bind(this);
|
||||
this.getNetworks = this.getNetworks.bind(this);
|
||||
this.getNetworksAsync = this.getNetworksAsync.bind(this);
|
||||
this.confirmDelete = this.confirmDelete.bind(this);
|
||||
this.confirmDeleteAsync = this.confirmDeleteAsync.bind(this);
|
||||
this.editorUpdate = this.editorUpdate.bind(this);
|
||||
this.isEditAllowed = this.isEditAllowed.bind(this);
|
||||
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
||||
this.onChangeTemplateVariables = this.onChangeTemplateVariables.bind(this);
|
||||
}
|
||||
|
||||
isEditAllowed(template) {
|
||||
return this.currentUser.isAdmin || this.currentUser.id === template.CreatedByUserId;
|
||||
}
|
||||
|
||||
getTemplates() {
|
||||
return this.$async(this.getTemplatesAsync);
|
||||
}
|
||||
async getTemplatesAsync() {
|
||||
try {
|
||||
const templates = await this.CustomTemplateService.customTemplates([1, 2]);
|
||||
this.templates = templates.filter((t) => !t.EdgeTemplate);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failed loading templates', err, 'Unable to load custom templates');
|
||||
}
|
||||
}
|
||||
|
||||
removeTemplates(templates) {
|
||||
return this.$async(this.removeTemplatesAsync, templates);
|
||||
}
|
||||
async removeTemplatesAsync(templates) {
|
||||
for (let template of templates) {
|
||||
try {
|
||||
await this.CustomTemplateService.remove(template.id);
|
||||
this.Notifications.success('Success', 'Removed template successfully');
|
||||
_.remove(this.templates, template);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failed removing template', err, 'Unable to remove custom template');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onChangeTemplateVariables(variables) {
|
||||
this.onChangeFormValues({ variables });
|
||||
|
||||
this.renderTemplate();
|
||||
}
|
||||
|
||||
renderTemplate() {
|
||||
if (!this.isTemplateVariablesEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileContent = renderTemplate(this.state.templateContent, this.formValues.variables, this.state.selectedTemplate.Variables);
|
||||
this.onChangeFormValues({ fileContent });
|
||||
}
|
||||
|
||||
onChangeFormValues(values) {
|
||||
this.formValues = {
|
||||
...this.formValues,
|
||||
...values,
|
||||
};
|
||||
}
|
||||
|
||||
validateForm(accessControlData, isAdmin) {
|
||||
this.state.formValidationError = '';
|
||||
const error = this.FormValidator.validateAccessControl(accessControlData, isAdmin);
|
||||
|
||||
if (error) {
|
||||
this.state.formValidationError = error;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
createStack() {
|
||||
return this.$async(this.createStackAsync);
|
||||
}
|
||||
async createStackAsync() {
|
||||
const userId = this.currentUser.id;
|
||||
const accessControlData = this.formValues.AccessControlData;
|
||||
|
||||
if (!this.validateForm(accessControlData, this.currentUser.isAdmin)) {
|
||||
return;
|
||||
}
|
||||
const stackName = this.formValues.name;
|
||||
|
||||
const endpointId = this.endpoint.Id;
|
||||
|
||||
this.state.actionInProgress = true;
|
||||
|
||||
try {
|
||||
const file = this.formValues.fileContent;
|
||||
const createAction = this.state.selectedTemplate.Type === 1 ? this.StackService.createSwarmStackFromFileContent : this.StackService.createComposeStackFromFileContent;
|
||||
const { ResourceControl: resourceControl } = await createAction(stackName, file, [], endpointId);
|
||||
await this.ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
|
||||
this.Notifications.success('Success', 'Stack successfully deployed');
|
||||
this.$state.go('docker.stacks');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Deployment error', err, 'Failed to deploy stack');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
unselectTemplate() {
|
||||
// wrapping unselect with async to make a digest cycle run between unselect to select
|
||||
return this.$async(this.unselectTemplateAsync);
|
||||
}
|
||||
async unselectTemplateAsync() {
|
||||
this.state.selectedTemplate = null;
|
||||
|
||||
this.formValues = {
|
||||
network: '',
|
||||
name: '',
|
||||
fileContent: '',
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
variables: [],
|
||||
};
|
||||
}
|
||||
|
||||
selectTemplate(templateId) {
|
||||
return this.$async(this.selectTemplateAsync, templateId);
|
||||
}
|
||||
async selectTemplateAsync(templateId) {
|
||||
if (this.state.selectedTemplate) {
|
||||
await this.unselectTemplate(this.state.selectedTemplate);
|
||||
}
|
||||
|
||||
const template = _.find(this.templates, { Id: templateId });
|
||||
|
||||
const isGit = template.GitConfig !== null;
|
||||
this.state.isEditorReadOnly = isGit;
|
||||
|
||||
try {
|
||||
this.state.templateContent = this.formValues.fileContent = await this.CustomTemplateService.customTemplateFile(template.Id, template.GitConfig !== null);
|
||||
} catch (err) {
|
||||
this.state.templateLoadFailed = true;
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve custom template data');
|
||||
}
|
||||
|
||||
this.formValues.network = _.find(this.availableNetworks, function (o) {
|
||||
return o.Name === 'bridge';
|
||||
});
|
||||
|
||||
this.formValues.name = template.Title ? template.Title : '';
|
||||
this.state.selectedTemplate = template;
|
||||
this.$anchorScroll('view-top');
|
||||
const applicationState = this.StateManager.getState();
|
||||
this.state.deployable = this.isDeployable(applicationState.endpoint, template.Type);
|
||||
|
||||
if (template.Variables && template.Variables.length > 0) {
|
||||
const variables = getVariablesFieldDefaultValues(template.Variables);
|
||||
this.onChangeTemplateVariables(variables);
|
||||
}
|
||||
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
getNetworks(provider, apiVersion) {
|
||||
return this.$async(this.getNetworksAsync, provider, apiVersion);
|
||||
}
|
||||
async getNetworksAsync(provider, apiVersion) {
|
||||
try {
|
||||
const networks = await this.NetworkService.networks(
|
||||
provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE',
|
||||
false,
|
||||
provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25
|
||||
);
|
||||
this.availableNetworks = networks;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Failed to load networks.');
|
||||
}
|
||||
}
|
||||
|
||||
confirmDelete(templateId) {
|
||||
return this.$async(this.confirmDeleteAsync, templateId);
|
||||
}
|
||||
async confirmDeleteAsync(templateId) {
|
||||
const confirmed = await confirmDelete('Are you sure that you want to delete this template?');
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var template = _.find(this.templates, { Id: templateId });
|
||||
await this.CustomTemplateService.remove(templateId);
|
||||
this.Notifications.success('Template successfully deleted', template && template.Title);
|
||||
this.templates = this.templates.filter((template) => template.Id !== templateId);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Failed to delete template');
|
||||
}
|
||||
}
|
||||
|
||||
editorUpdate(value) {
|
||||
this.formValues.fileContent = value;
|
||||
}
|
||||
|
||||
isDeployable(endpoint, templateType) {
|
||||
let deployable = false;
|
||||
switch (templateType) {
|
||||
case 1:
|
||||
deployable = endpoint.mode.provider === this.DOCKER_SWARM_MODE;
|
||||
break;
|
||||
case 2:
|
||||
deployable = endpoint.mode.provider === this.DOCKER_STANDALONE;
|
||||
break;
|
||||
}
|
||||
|
||||
return deployable;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
const applicationState = this.StateManager.getState();
|
||||
|
||||
const {
|
||||
endpoint: { mode: endpointMode },
|
||||
apiVersion,
|
||||
} = applicationState;
|
||||
|
||||
this.getTemplates();
|
||||
this.getNetworks(endpointMode.provider, apiVersion);
|
||||
|
||||
this.currentUser.isAdmin = this.Authentication.isAdmin();
|
||||
const user = this.Authentication.getUserDetails();
|
||||
this.currentUser.id = user.ID;
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomTemplatesViewController;
|
|
@ -1,9 +0,0 @@
|
|||
import CustomTemplatesViewController from './customTemplatesViewController.js';
|
||||
|
||||
angular.module('portainer.app').component('customTemplatesView', {
|
||||
templateUrl: './customTemplatesView.html',
|
||||
controller: CustomTemplatesViewController,
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue