mirror of
https://github.com/portainer/portainer.git
synced 2025-08-04 05:15:25 +02:00
refactor(containers): migrate create view to react [EE-2307] (#9175)
This commit is contained in:
parent
bc0050a7b4
commit
d970f0e2bc
71 changed files with 2612 additions and 1399 deletions
|
@ -1,47 +1,9 @@
|
|||
import angular from 'angular';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||
import { withFormValidation } from '@/react-tools/withFormValidation';
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { ContainerNetworksDatatable } from '@/react/docker/containers/ItemView/ContainerNetworksDatatable';
|
||||
import {
|
||||
CommandsTab,
|
||||
CommandsTabValues,
|
||||
commandsTabValidation,
|
||||
} from '@/react/docker/containers/CreateView/CommandsTab';
|
||||
import {
|
||||
EnvVarsTab,
|
||||
envVarsTabUtils,
|
||||
} from '@/react/docker/containers/CreateView/EnvVarsTab';
|
||||
import {
|
||||
VolumesTab,
|
||||
volumesTabUtils,
|
||||
} from '@/react/docker/containers/CreateView/VolumesTab';
|
||||
import {
|
||||
networkTabUtils,
|
||||
NetworkTab,
|
||||
type NetworkTabValues,
|
||||
} from '@/react/docker/containers/CreateView/NetworkTab';
|
||||
import {
|
||||
ResourcesTab,
|
||||
resourcesTabUtils,
|
||||
type ResourcesTabValues,
|
||||
} from '@/react/docker/containers/CreateView/ResourcesTab';
|
||||
import {
|
||||
CapabilitiesTab,
|
||||
capabilitiesTabUtils,
|
||||
} from '@/react/docker/containers/CreateView/CapabilitiesTab';
|
||||
import {
|
||||
RestartPolicyTab,
|
||||
restartPolicyTabUtils,
|
||||
} from '@/react/docker/containers/CreateView/RestartPolicyTab';
|
||||
import {
|
||||
LabelsTab,
|
||||
labelsTabUtils,
|
||||
} from '@/react/docker/containers/CreateView/LabelsTab';
|
||||
|
||||
const ngModule = angular
|
||||
.module('portainer.docker.react.components.containers', [])
|
||||
|
@ -55,74 +17,3 @@ const ngModule = angular
|
|||
);
|
||||
|
||||
export const containersModule = ngModule.name;
|
||||
|
||||
withFormValidation<ComponentProps<typeof CommandsTab>, CommandsTabValues>(
|
||||
ngModule,
|
||||
withUIRouter(withReactQuery(CommandsTab)),
|
||||
'dockerCreateContainerCommandsTab',
|
||||
['apiVersion'],
|
||||
commandsTabValidation
|
||||
);
|
||||
|
||||
withFormValidation(
|
||||
ngModule,
|
||||
withUIRouter(withReactQuery(EnvVarsTab)),
|
||||
'dockerCreateContainerEnvVarsTab',
|
||||
[],
|
||||
envVarsTabUtils.validation
|
||||
);
|
||||
|
||||
withFormValidation(
|
||||
ngModule,
|
||||
withUIRouter(withReactQuery(VolumesTab)),
|
||||
'dockerCreateContainerVolumesTab',
|
||||
['allowBindMounts'],
|
||||
volumesTabUtils.validation
|
||||
);
|
||||
|
||||
withFormValidation<ComponentProps<typeof NetworkTab>, NetworkTabValues>(
|
||||
ngModule,
|
||||
withUIRouter(withReactQuery(NetworkTab)),
|
||||
'dockerCreateContainerNetworkTab',
|
||||
[],
|
||||
networkTabUtils.validation
|
||||
);
|
||||
|
||||
withFormValidation<ComponentProps<typeof ResourcesTab>, ResourcesTabValues>(
|
||||
ngModule,
|
||||
withUIRouter(withReactQuery(ResourcesTab)),
|
||||
'dockerCreateContainerResourcesTab',
|
||||
[
|
||||
'allowPrivilegedMode',
|
||||
'isDevicesFieldVisible',
|
||||
'isInitFieldVisible',
|
||||
'isSysctlFieldVisible',
|
||||
'isDuplicate',
|
||||
'isImageInvalid',
|
||||
'redeploy',
|
||||
],
|
||||
resourcesTabUtils.validation
|
||||
);
|
||||
|
||||
withFormValidation(
|
||||
ngModule,
|
||||
CapabilitiesTab,
|
||||
'dockerCreateContainerCapabilitiesTab',
|
||||
[],
|
||||
capabilitiesTabUtils.validation
|
||||
);
|
||||
withFormValidation(
|
||||
ngModule,
|
||||
RestartPolicyTab,
|
||||
'dockerCreateContainerRestartPolicyTab',
|
||||
[],
|
||||
restartPolicyTabUtils.validation
|
||||
);
|
||||
|
||||
withFormValidation(
|
||||
ngModule,
|
||||
withUIRouter(withReactQuery(LabelsTab)),
|
||||
'dockerCreateContainerLabelsTab',
|
||||
[],
|
||||
labelsTabUtils.validation
|
||||
);
|
||||
|
|
|
@ -7,9 +7,14 @@ import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
|||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { LogView } from '@/react/docker/containers/LogView';
|
||||
import { CreateView } from '@/react/docker/containers/CreateView';
|
||||
|
||||
export const containersModule = angular
|
||||
.module('portainer.docker.react.views.containers', [])
|
||||
.component(
|
||||
'createContainerView',
|
||||
r2a(withUIRouter(withCurrentUser(CreateView)), [])
|
||||
)
|
||||
.component(
|
||||
'containersView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(ListView))), ['endpoint'])
|
||||
|
@ -77,8 +82,7 @@ function config($stateRegistryProvider: StateRegistry) {
|
|||
url: '/new?nodeName&from',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: '~@/docker/views/containers/create/createcontainer.html',
|
||||
controller: 'CreateContainerController',
|
||||
component: 'createContainerView',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,686 +0,0 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
||||
|
||||
import { confirmDestructive } from '@@/modals/confirm';
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
|
||||
import { commandsTabUtils } from '@/react/docker/containers/CreateView/CommandsTab';
|
||||
import { volumesTabUtils } from '@/react/docker/containers/CreateView/VolumesTab';
|
||||
import { networkTabUtils } from '@/react/docker/containers/CreateView/NetworkTab';
|
||||
import { capabilitiesTabUtils } from '@/react/docker/containers/CreateView/CapabilitiesTab';
|
||||
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { ContainerDetailsViewModel } from '@/docker/models/container';
|
||||
import { labelsTabUtils } from '@/react/docker/containers/CreateView/LabelsTab';
|
||||
|
||||
import './createcontainer.css';
|
||||
import { envVarsTabUtils } from '@/react/docker/containers/CreateView/EnvVarsTab';
|
||||
import { getContainers } from '@/react/docker/containers/queries/containers';
|
||||
import { resourcesTabUtils } from '@/react/docker/containers/CreateView/ResourcesTab';
|
||||
import { restartPolicyTabUtils } from '@/react/docker/containers/CreateView/RestartPolicyTab';
|
||||
|
||||
angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
'$q',
|
||||
'$scope',
|
||||
'$async',
|
||||
'$state',
|
||||
'$timeout',
|
||||
'$transition$',
|
||||
'$analytics',
|
||||
'Container',
|
||||
'ContainerHelper',
|
||||
'ImageHelper',
|
||||
'NetworkService',
|
||||
'ResourceControlService',
|
||||
'Authentication',
|
||||
'Notifications',
|
||||
'ContainerService',
|
||||
'ImageService',
|
||||
'FormValidator',
|
||||
'RegistryService',
|
||||
'SystemService',
|
||||
'SettingsService',
|
||||
'HttpRequestHelper',
|
||||
'endpoint',
|
||||
function (
|
||||
$q,
|
||||
$scope,
|
||||
$async,
|
||||
$state,
|
||||
$timeout,
|
||||
$transition$,
|
||||
$analytics,
|
||||
Container,
|
||||
ContainerHelper,
|
||||
ImageHelper,
|
||||
NetworkService,
|
||||
ResourceControlService,
|
||||
Authentication,
|
||||
Notifications,
|
||||
ContainerService,
|
||||
ImageService,
|
||||
FormValidator,
|
||||
RegistryService,
|
||||
SystemService,
|
||||
SettingsService,
|
||||
HttpRequestHelper,
|
||||
endpoint
|
||||
) {
|
||||
$scope.create = create;
|
||||
$scope.endpoint = endpoint;
|
||||
$scope.containerWebhookFeature = FeatureId.CONTAINER_WEBHOOK;
|
||||
$scope.formValues = {
|
||||
alwaysPull: true,
|
||||
GPU: {
|
||||
enabled: false,
|
||||
useSpecific: false,
|
||||
selectedGPUs: ['all'],
|
||||
capabilities: ['compute', 'utility'],
|
||||
},
|
||||
ExtraHosts: [],
|
||||
MacAddress: '',
|
||||
IPv4: '',
|
||||
IPv6: '',
|
||||
DnsPrimary: '',
|
||||
DnsSecondary: '',
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
NodeName: null,
|
||||
RegistryModel: new PorImageRegistryModel(),
|
||||
|
||||
commands: commandsTabUtils.getDefaultViewModel(),
|
||||
envVars: envVarsTabUtils.getDefaultViewModel(),
|
||||
volumes: volumesTabUtils.getDefaultViewModel(),
|
||||
network: networkTabUtils.getDefaultViewModel(),
|
||||
resources: resourcesTabUtils.getDefaultViewModel(),
|
||||
capabilities: capabilitiesTabUtils.getDefaultViewModel(),
|
||||
restartPolicy: restartPolicyTabUtils.getDefaultViewModel(),
|
||||
labels: labelsTabUtils.getDefaultViewModel(),
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
formValidationError: '',
|
||||
actionInProgress: false,
|
||||
mode: '',
|
||||
pullImageValidity: true,
|
||||
settingUnlimitedResources: false,
|
||||
containerIsLoaded: false,
|
||||
};
|
||||
|
||||
$scope.onAlwaysPullChange = onAlwaysPullChange;
|
||||
$scope.handlePublishAllPortsChange = handlePublishAllPortsChange;
|
||||
$scope.handleAutoRemoveChange = handleAutoRemoveChange;
|
||||
$scope.handlePrivilegedChange = handlePrivilegedChange;
|
||||
$scope.handleInitChange = handleInitChange;
|
||||
$scope.handleCommandsChange = handleCommandsChange;
|
||||
$scope.handleEnvVarsChange = handleEnvVarsChange;
|
||||
|
||||
function handleCommandsChange(commands) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.formValues.commands = commands;
|
||||
});
|
||||
}
|
||||
|
||||
function handleEnvVarsChange(value) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.formValues.envVars = value;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.onVolumesChange = function (volumes) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.formValues.volumes = volumes;
|
||||
});
|
||||
};
|
||||
$scope.onNetworkChange = function (network) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.formValues.network = network;
|
||||
});
|
||||
};
|
||||
$scope.onLabelsChange = function (labels) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.formValues.labels = labels;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onResourcesChange = function (resources) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.formValues.resources = resources;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onCapabilitiesChange = function (capabilities) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.formValues.capabilities = capabilities;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onRestartPolicyChange = function (restartPolicy) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.formValues.restartPolicy = restartPolicy;
|
||||
});
|
||||
};
|
||||
|
||||
function onAlwaysPullChange(checked) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.formValues.alwaysPull = checked;
|
||||
});
|
||||
}
|
||||
|
||||
function handlePublishAllPortsChange(checked) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.config.HostConfig.PublishAllPorts = checked;
|
||||
});
|
||||
}
|
||||
|
||||
function handleAutoRemoveChange(checked) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.config.HostConfig.AutoRemove = checked;
|
||||
});
|
||||
}
|
||||
|
||||
function handlePrivilegedChange(checked) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.config.HostConfig.Privileged = checked;
|
||||
});
|
||||
}
|
||||
|
||||
function handleInitChange(checked) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.config.HostConfig.Init = checked;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.refreshSlider = function () {
|
||||
$timeout(function () {
|
||||
$scope.$broadcast('rzSliderForceRender');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onImageNameChange = function () {
|
||||
$scope.formValues.CmdMode = 'default';
|
||||
$scope.formValues.EntrypointMode = 'default';
|
||||
};
|
||||
|
||||
$scope.setPullImageValidity = setPullImageValidity;
|
||||
function setPullImageValidity(validity) {
|
||||
if (!validity) {
|
||||
$scope.formValues.alwaysPull = false;
|
||||
}
|
||||
$scope.state.pullImageValidity = validity;
|
||||
}
|
||||
|
||||
$scope.config = {
|
||||
Image: '',
|
||||
Env: [],
|
||||
Cmd: null,
|
||||
MacAddress: '',
|
||||
ExposedPorts: {},
|
||||
Entrypoint: null,
|
||||
WorkingDir: '',
|
||||
User: '',
|
||||
HostConfig: {
|
||||
RestartPolicy: {
|
||||
Name: 'no',
|
||||
},
|
||||
PortBindings: [],
|
||||
PublishAllPorts: false,
|
||||
Binds: [],
|
||||
AutoRemove: false,
|
||||
NetworkMode: 'bridge',
|
||||
Privileged: false,
|
||||
Init: false,
|
||||
Runtime: null,
|
||||
ExtraHosts: [],
|
||||
Devices: [],
|
||||
DeviceRequests: [],
|
||||
CapAdd: [],
|
||||
CapDrop: [],
|
||||
Sysctls: {},
|
||||
LogConfig: {
|
||||
Type: '',
|
||||
Config: {},
|
||||
},
|
||||
},
|
||||
NetworkingConfig: {
|
||||
EndpointsConfig: {},
|
||||
},
|
||||
Labels: {},
|
||||
};
|
||||
|
||||
$scope.addPortBinding = function () {
|
||||
$scope.config.HostConfig.PortBindings.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
|
||||
};
|
||||
|
||||
$scope.removePortBinding = function (index) {
|
||||
$scope.config.HostConfig.PortBindings.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addExtraHost = function () {
|
||||
$scope.formValues.ExtraHosts.push({ value: '' });
|
||||
};
|
||||
|
||||
$scope.removeExtraHost = function (index) {
|
||||
$scope.formValues.ExtraHosts.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addDevice = function () {
|
||||
$scope.config.HostConfig.Devices.push({ pathOnHost: '', pathInContainer: '' });
|
||||
};
|
||||
|
||||
$scope.removeDevice = function (index) {
|
||||
$scope.config.HostConfig.Devices.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.onGpuChange = function (values) {
|
||||
return $async(async () => {
|
||||
$scope.formValues.GPU = values;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addSysctl = function () {
|
||||
$scope.formValues.Sysctls.push({ name: '', value: '' });
|
||||
};
|
||||
|
||||
$scope.removeSysctl = function (index) {
|
||||
$scope.formValues.Sysctls.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.fromContainerMultipleNetworks = false;
|
||||
|
||||
function prepareImageConfig(config) {
|
||||
const imageConfig = ImageHelper.createImageConfigForContainer($scope.formValues.RegistryModel);
|
||||
config.Image = imageConfig.fromImage;
|
||||
}
|
||||
|
||||
function preparePortBindings(config) {
|
||||
const bindings = ContainerHelper.preparePortBindings(config.HostConfig.PortBindings);
|
||||
config.ExposedPorts = {};
|
||||
_.forEach(bindings, (_, key) => (config.ExposedPorts[key] = {}));
|
||||
config.HostConfig.PortBindings = bindings;
|
||||
}
|
||||
|
||||
function prepareConfiguration() {
|
||||
var config = angular.copy($scope.config);
|
||||
config = commandsTabUtils.toRequest(config, $scope.formValues.commands);
|
||||
config = envVarsTabUtils.toRequest(config, $scope.formValues.envVars);
|
||||
config = volumesTabUtils.toRequest(config, $scope.formValues.volumes);
|
||||
config = networkTabUtils.toRequest(config, $scope.formValues.network, $scope.fromContainer.Id);
|
||||
config = resourcesTabUtils.toRequest(config, $scope.formValues.resources);
|
||||
config = capabilitiesTabUtils.toRequest(config, $scope.formValues.capabilities);
|
||||
config = restartPolicyTabUtils.toRequest(config, $scope.formValues.restartPolicy);
|
||||
config = labelsTabUtils.toRequest(config, $scope.formValues.labels);
|
||||
|
||||
prepareImageConfig(config);
|
||||
preparePortBindings(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
function loadFromContainerPortBindings() {
|
||||
const bindings = ContainerHelper.sortAndCombinePorts($scope.config.HostConfig.PortBindings);
|
||||
$scope.config.HostConfig.PortBindings = bindings;
|
||||
}
|
||||
|
||||
function loadFromContainerImageConfig() {
|
||||
RegistryService.retrievePorRegistryModelFromRepository($scope.config.Image, endpoint.Id)
|
||||
.then((model) => {
|
||||
$scope.formValues.RegistryModel = model;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve registry');
|
||||
});
|
||||
}
|
||||
|
||||
function loadFromContainerSpec() {
|
||||
// Get container
|
||||
Container.get({ id: $transition$.params().from })
|
||||
.$promise.then(function success(d) {
|
||||
var fromContainer = new ContainerDetailsViewModel(d);
|
||||
if (fromContainer.ResourceControl) {
|
||||
if (fromContainer.ResourceControl.Public) {
|
||||
$scope.formValues.AccessControlData.AccessControlEnabled = false;
|
||||
}
|
||||
|
||||
// When the container is create by duplicate/edit, the access permission
|
||||
// shouldn't be copied
|
||||
fromContainer.ResourceControl.UserAccesses = [];
|
||||
fromContainer.ResourceControl.TeamAccesses = [];
|
||||
}
|
||||
|
||||
$scope.fromContainer = fromContainer;
|
||||
$scope.state.mode = 'duplicate';
|
||||
$scope.config = ContainerHelper.configFromContainer(angular.copy(d));
|
||||
|
||||
$scope.formValues.commands = commandsTabUtils.toViewModel(d);
|
||||
$scope.formValues.envVars = envVarsTabUtils.toViewModel(d);
|
||||
$scope.formValues.volumes = volumesTabUtils.toViewModel(d);
|
||||
$scope.formValues.network = networkTabUtils.toViewModel(d, $scope.availableNetworks, $scope.runningContainers);
|
||||
$scope.formValues.resources = resourcesTabUtils.toViewModel(d);
|
||||
$scope.formValues.capabilities = capabilitiesTabUtils.toViewModel(d);
|
||||
$scope.formValues.labels = labelsTabUtils.toViewModel(d);
|
||||
|
||||
$scope.formValues.restartPolicy = restartPolicyTabUtils.toViewModel(d);
|
||||
|
||||
loadFromContainerPortBindings(d);
|
||||
loadFromContainerImageConfig(d);
|
||||
})
|
||||
.then(() => {
|
||||
$scope.state.containerIsLoaded = true;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve container');
|
||||
});
|
||||
}
|
||||
|
||||
async function initView() {
|
||||
var nodeName = $transition$.params().nodeName;
|
||||
$scope.formValues.NodeName = nodeName;
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||
|
||||
$scope.isAdmin = Authentication.isAdmin();
|
||||
$scope.showDeviceMapping = await shouldShowDevices();
|
||||
$scope.allowSysctl = await shouldShowSysctls();
|
||||
$scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled();
|
||||
$scope.isAdminOrEndpointAdmin = Authentication.isAdmin();
|
||||
|
||||
var provider = $scope.applicationState.endpoint.mode.provider;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
NetworkService.networks(provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', false, provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25)
|
||||
.then(function success(networks) {
|
||||
networks.push({ Name: 'container' });
|
||||
$scope.availableNetworks = networks.sort((a, b) => a.Name.localeCompare(b.Name));
|
||||
|
||||
$scope.formValues.network = networkTabUtils.getDefaultViewModel(networks.some((network) => network.Name === 'bridge'));
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve networks');
|
||||
});
|
||||
getContainers(endpoint.Id)
|
||||
.then((containers) => {
|
||||
$scope.runningContainers = containers;
|
||||
$scope.gpuUseAll = _.get($scope, 'endpoint.Snapshots[0].GpuUseAll', false);
|
||||
$scope.gpuUseList = _.get($scope, 'endpoint.Snapshots[0].GpuUseList', []);
|
||||
if ($transition$.params().from) {
|
||||
loadFromContainerSpec();
|
||||
} else {
|
||||
$scope.state.containerIsLoaded = true;
|
||||
$scope.fromContainer = {};
|
||||
if ($scope.areContainerCapabilitiesEnabled) {
|
||||
$scope.formValues.capabilities = capabilitiesTabUtils.getDefaultViewModel();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve running containers');
|
||||
});
|
||||
|
||||
SystemService.info()
|
||||
.then(function success(data) {
|
||||
$scope.availableRuntimes = data.Runtimes ? Object.keys(data.Runtimes) : [];
|
||||
$scope.state.sliderMaxCpu = 32;
|
||||
if (data.NCPU) {
|
||||
$scope.state.sliderMaxCpu = data.NCPU;
|
||||
}
|
||||
$scope.state.sliderMaxMemory = 32768;
|
||||
if (data.MemTotal) {
|
||||
$scope.state.sliderMaxMemory = Math.floor(data.MemTotal / 1000 / 1000);
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve engine details');
|
||||
});
|
||||
|
||||
$scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || endpoint.SecuritySettings.allowBindMountsForRegularUsers;
|
||||
$scope.allowPrivilegedMode = endpoint.SecuritySettings.allowPrivilegedModeForRegularUsers;
|
||||
}
|
||||
|
||||
function validateForm(accessControlData, isAdmin) {
|
||||
$scope.state.formValidationError = '';
|
||||
var error = '';
|
||||
error = FormValidator.validateAccessControl(accessControlData, isAdmin);
|
||||
|
||||
if (error) {
|
||||
$scope.state.formValidationError = error;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
$scope.handleResourceChange = handleResourceChange;
|
||||
function handleResourceChange() {
|
||||
$scope.state.settingUnlimitedResources = false;
|
||||
if (
|
||||
($scope.config.HostConfig.Memory > 0 && $scope.formValues.MemoryLimit === 0) ||
|
||||
($scope.config.HostConfig.MemoryReservation > 0 && $scope.formValues.MemoryReservation === 0) ||
|
||||
($scope.config.HostConfig.NanoCpus > 0 && $scope.formValues.CpuLimit === 0)
|
||||
) {
|
||||
$scope.state.settingUnlimitedResources = true;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.redeployUnlimitedResources = function (resources) {
|
||||
return $async(async () => {
|
||||
$scope.formValues.resources = resources;
|
||||
return create();
|
||||
});
|
||||
};
|
||||
|
||||
function create() {
|
||||
var oldContainer = null;
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader($scope.formValues.NodeName);
|
||||
return findCurrentContainer().then(setOldContainer).then(confirmCreateContainer).then(startCreationProcess).catch(notifyOnError).finally(final);
|
||||
|
||||
function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
}
|
||||
|
||||
function setOldContainer(container) {
|
||||
oldContainer = container;
|
||||
return container;
|
||||
}
|
||||
|
||||
function findCurrentContainer() {
|
||||
return Container.query({ all: 1, filters: { name: ['^/' + $scope.config.name + '$'] } })
|
||||
.$promise.then(function onQuerySuccess(containers) {
|
||||
if (!containers.length) {
|
||||
return;
|
||||
}
|
||||
return containers[0];
|
||||
})
|
||||
.catch(notifyOnError);
|
||||
|
||||
function notifyOnError(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve containers');
|
||||
}
|
||||
}
|
||||
|
||||
function startCreationProcess(confirmed) {
|
||||
if (!confirmed) {
|
||||
return $q.when();
|
||||
}
|
||||
if (!validateAccessControl()) {
|
||||
return $q.when();
|
||||
}
|
||||
$scope.state.actionInProgress = true;
|
||||
return pullImageIfNeeded()
|
||||
.then(stopAndRenameContainer)
|
||||
.then(createNewContainer)
|
||||
.then(applyResourceControl)
|
||||
.then(connectToExtraNetworks)
|
||||
.then(removeOldContainer)
|
||||
.then(onSuccess)
|
||||
.catch(onCreationProcessFail);
|
||||
}
|
||||
|
||||
function onCreationProcessFail(error) {
|
||||
var deferred = $q.defer();
|
||||
removeNewContainer()
|
||||
.then(restoreOldContainerName)
|
||||
.then(function () {
|
||||
deferred.reject(error);
|
||||
})
|
||||
.catch(function (restoreError) {
|
||||
deferred.reject(restoreError);
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function removeNewContainer() {
|
||||
return findCurrentContainer().then(function onContainerLoaded(container) {
|
||||
if (container && (!oldContainer || container.Id !== oldContainer.Id)) {
|
||||
return ContainerService.remove(endpoint.Id, container, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function restoreOldContainerName() {
|
||||
if (!oldContainer) {
|
||||
return;
|
||||
}
|
||||
return ContainerService.renameContainer(endpoint.Id, oldContainer.Id, oldContainer.Names[0]);
|
||||
}
|
||||
|
||||
function confirmCreateContainer(container) {
|
||||
if (!container) {
|
||||
return $q.when(true);
|
||||
}
|
||||
|
||||
return showConfirmationModal();
|
||||
|
||||
function showConfirmationModal() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
confirmDestructive({
|
||||
title: 'Are you sure?',
|
||||
message: 'A container with the same name already exists. Portainer can automatically remove it and re-create one. Do you want to replace it?',
|
||||
confirmButton: buildConfirmButton('Replace', 'danger'),
|
||||
}).then(function onConfirm(confirmed) {
|
||||
deferred.resolve(confirmed);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
}
|
||||
|
||||
function stopAndRenameContainer() {
|
||||
if (!oldContainer) {
|
||||
return $q.when();
|
||||
}
|
||||
return stopContainerIfNeeded(oldContainer).then(renameContainer);
|
||||
}
|
||||
|
||||
function stopContainerIfNeeded(oldContainer) {
|
||||
if (oldContainer.State !== 'running') {
|
||||
return $q.when();
|
||||
}
|
||||
return ContainerService.stopContainer(endpoint.Id, oldContainer.Id);
|
||||
}
|
||||
|
||||
function renameContainer() {
|
||||
return ContainerService.renameContainer(endpoint.Id, oldContainer.Id, oldContainer.Names[0] + '-old');
|
||||
}
|
||||
|
||||
function pullImageIfNeeded() {
|
||||
return $q.when($scope.formValues.alwaysPull && ImageService.pullImage($scope.formValues.RegistryModel, true));
|
||||
}
|
||||
|
||||
function createNewContainer() {
|
||||
return $async(async () => {
|
||||
const config = prepareConfiguration();
|
||||
return await ContainerService.createAndStartContainer(endpoint.Id, config);
|
||||
});
|
||||
}
|
||||
|
||||
async function sendAnalytics() {
|
||||
const publicSettings = await SettingsService.publicSettings();
|
||||
const analyticsAllowed = publicSettings.EnableTelemetry;
|
||||
const image = `${$scope.formValues.RegistryModel.Registry.URL}/${$scope.formValues.RegistryModel.Image}`;
|
||||
if (analyticsAllowed && $scope.formValues.GPU.enabled) {
|
||||
$analytics.eventTrack('gpuContainerCreated', {
|
||||
category: 'docker',
|
||||
metadata: { gpu: $scope.formValues.GPU, containerImage: image },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function applyResourceControl(newContainer) {
|
||||
const userId = Authentication.getUserDetails().ID;
|
||||
const resourceControl = newContainer.Portainer.ResourceControl;
|
||||
const containerId = newContainer.Id;
|
||||
const accessControlData = $scope.formValues.AccessControlData;
|
||||
|
||||
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl).then(function onApplyResourceControlSuccess() {
|
||||
return containerId;
|
||||
});
|
||||
}
|
||||
|
||||
function connectToExtraNetworks(newContainerId) {
|
||||
if (!$scope.formValues.network.extraNetworks) {
|
||||
return $q.when();
|
||||
}
|
||||
|
||||
var connectionPromises = _.forOwn($scope.formValues.network.extraNetworks, function (network, networkName) {
|
||||
if (_.has(network, 'Aliases')) {
|
||||
var aliases = _.filter(network.Aliases, (o) => {
|
||||
return !_.startsWith($scope.fromContainer.Id, o);
|
||||
});
|
||||
}
|
||||
return NetworkService.connectContainer(networkName, newContainerId, aliases);
|
||||
});
|
||||
|
||||
return $q.all(connectionPromises);
|
||||
}
|
||||
|
||||
function removeOldContainer() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
if (!oldContainer) {
|
||||
deferred.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
ContainerService.remove(endpoint.Id, oldContainer, true).then(notifyOnRemoval).catch(notifyOnRemoveError);
|
||||
|
||||
return deferred.promise;
|
||||
|
||||
function notifyOnRemoval() {
|
||||
Notifications.success('Container Removed', oldContainer.Id);
|
||||
deferred.resolve();
|
||||
}
|
||||
|
||||
function notifyOnRemoveError(err) {
|
||||
deferred.reject({ msg: 'Unable to remove container', err: err });
|
||||
}
|
||||
}
|
||||
|
||||
function notifyOnError(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create container');
|
||||
}
|
||||
|
||||
function validateAccessControl() {
|
||||
var accessControlData = $scope.formValues.AccessControlData;
|
||||
return validateForm(accessControlData, $scope.isAdmin);
|
||||
}
|
||||
|
||||
async function onSuccess() {
|
||||
await sendAnalytics();
|
||||
Notifications.success('Success', 'Container successfully created');
|
||||
$state.go('docker.containers', {}, { reload: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function shouldShowDevices() {
|
||||
return endpoint.SecuritySettings.allowDeviceMappingForRegularUsers || Authentication.isAdmin();
|
||||
}
|
||||
|
||||
async function shouldShowSysctls() {
|
||||
return endpoint.SecuritySettings.allowSysctlSettingForRegularUsers || Authentication.isAdmin();
|
||||
}
|
||||
|
||||
async function checkIfContainerCapabilitiesEnabled() {
|
||||
return endpoint.SecuritySettings.allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin();
|
||||
}
|
||||
|
||||
initView();
|
||||
},
|
||||
]);
|
|
@ -1,8 +0,0 @@
|
|||
.edit-resources {
|
||||
padding: 20px;
|
||||
border: 1px solid var(--border-widget-color);
|
||||
}
|
||||
|
||||
.widget .edit-resources button {
|
||||
margin-left: 0;
|
||||
}
|
|
@ -1,273 +0,0 @@
|
|||
<page-header title="'Create container'" breadcrumbs="[{label:'Containers', link:'docker.containers'}, 'Add container']"> </page-header>
|
||||
|
||||
<information-panel title-text="Caution" ng-if="state.mode == 'duplicate'">
|
||||
<span class="small">
|
||||
<p class="text-muted">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'" class-name="'mr-0.5'"></pr-icon>
|
||||
The new container may fail to start if the image is changed, and settings from the previous container aren't compatible. Common causes include entrypoint, cmd or
|
||||
<a href="https://docs.portainer.io/user/docker/containers/advanced" target="_blank">other settings</a> set by an image.
|
||||
</p>
|
||||
</span>
|
||||
</information-panel>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" autocomplete="off">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control" ng-model="config.name" id="container_name" placeholder="e.g. myContainer" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<div class="col-sm-12 form-section-title"> Image configuration </div>
|
||||
<div ng-if="!formValues.RegistryModel.Registry && fromContainer">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
||||
<span class="small text-danger" style="margin-left: 5px">
|
||||
The Docker registry for the <code>{{ config.Image }}</code> image is not registered inside Portainer, you will not be able to create a container. Please register that
|
||||
registry first.
|
||||
</span>
|
||||
</div>
|
||||
<div ng-if="formValues.RegistryModel.Registry || !fromContainer">
|
||||
<!-- image-and-registry -->
|
||||
<por-image-registry
|
||||
model="formValues.RegistryModel"
|
||||
ng-if="formValues.RegistryModel.Registry"
|
||||
auto-complete="true"
|
||||
endpoint="endpoint"
|
||||
is-admin="isAdmin"
|
||||
check-rate-limits="formValues.alwaysPull"
|
||||
on-image-change="onImageNameChange()"
|
||||
set-validity="setPullImageValidity"
|
||||
>
|
||||
<!-- always-pull -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
name="'alwaysPull'"
|
||||
label-class="'col-sm-2'"
|
||||
checked="formValues.alwaysPull"
|
||||
disabled="!state.pullImageValidity"
|
||||
label="'Always pull the image'"
|
||||
on-change="(onAlwaysPullChange)"
|
||||
tooltip="'When enabled, Portainer will automatically try to pull the specified image before creating the container.'"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !always-pull -->
|
||||
</por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
</div>
|
||||
|
||||
<!-- create-webhook -->
|
||||
<div ng-if="isAdmin && applicationState.endpoint.type !== 4">
|
||||
<div class="col-sm-12 form-section-title"> Webhooks </div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
feature-id="'container-webhook'"
|
||||
label-class="'col-sm-2'"
|
||||
label="'Create a container webhook'"
|
||||
tooltip="'Create a webhook (or callback URI) to automate the recreate this container. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and recreate this container.'"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !create-webhook -->
|
||||
|
||||
<div class="col-sm-12 form-section-title"> Network ports configuration </div>
|
||||
<!-- publish-exposed-ports -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
label-class="'col-sm-2'"
|
||||
checked="config.HostConfig.PublishAllPorts"
|
||||
label="'Publish all exposed network ports to random host ports'"
|
||||
on-change="(handlePublishAllPortsChange)"
|
||||
tooltip="'When enabled, Portainer will let Docker automatically map a random port on the host to each one defined in the image Dockerfile.'"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !publish-exposed-ports -->
|
||||
<!-- port-mapping -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Manual network port publishing
|
||||
<portainer-tooltip
|
||||
message="'When a range of ports on the host and a single port on the container is specified, Docker will randomly choose a single available port in the defined range and forward that to the container port.'"
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px" ng-click="addPortBinding()">
|
||||
<pr-icon icon="'plus'" mode="'alt'"></pr-icon> publish a new network port
|
||||
</span>
|
||||
</div>
|
||||
<!-- port-mapping-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px">
|
||||
<div ng-repeat="portBinding in config.HostConfig.PortBindings" style="margin-top: 2px">
|
||||
<!-- host-port -->
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80, 80-88, ip:80 or ip:80-88 (optional)" />
|
||||
</div>
|
||||
<!-- !host-port -->
|
||||
<span style="margin: 0 10px 0 10px">
|
||||
<pr-icon icon="'arrow-right'"></pr-icon>
|
||||
</span>
|
||||
<!-- container-port -->
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80 or 80-88" />
|
||||
</div>
|
||||
<!-- !container-port -->
|
||||
<!-- protocol-actions -->
|
||||
<div class="input-group col-sm-3 input-group-sm">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-light" ng-model="portBinding.protocol" uib-btn-radio="'tcp'">TCP</label>
|
||||
<label class="btn btn-light" ng-model="portBinding.protocol" uib-btn-radio="'udp'">UDP</label>
|
||||
</div>
|
||||
<button class="btn btn-light" type="button" ng-click="removePortBinding($index)">
|
||||
<pr-icon icon="'trash-2'" class-name="'icon-secondary icon-md'"></pr-icon>
|
||||
</button>
|
||||
</div>
|
||||
<!-- !protocol-actions -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !port-mapping-input-list -->
|
||||
</div>
|
||||
<!-- !port-mapping -->
|
||||
<div ng-if="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<div class="col-sm-12 form-section-title"> Deployment </div>
|
||||
<!-- node-selection -->
|
||||
<node-selector model="formValues.NodeName" endpoint-id="endpoint.Id"> </node-selector>
|
||||
<!-- !node-selection -->
|
||||
</div>
|
||||
<!-- access-control -->
|
||||
<por-access-control-form form-data="formValues.AccessControlData" resource-control="fromContainer.ResourceControl" ng-if="fromContainer"></por-access-control-form>
|
||||
<!-- !access-control -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title"> Actions </div>
|
||||
<!-- autoremove -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
label-class="'col-sm-2'"
|
||||
checked="config.HostConfig.AutoRemove"
|
||||
label="'Auto remove'"
|
||||
on-change="(handleAutoRemoveChange)"
|
||||
tooltip="'When enabled, Portainer will automatically remove the container when it exits. This is useful when you want to use the container only once.'"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !autoremove -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm !ml-0"
|
||||
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image || (!formValues.RegistryModel.Registry && fromContainer)
|
||||
|| (fromContainer.IsPortainer && fromContainer.Name === '/' + config.name)"
|
||||
ng-click="create()"
|
||||
button-spinner="state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="state.actionInProgress">Deploy the container</span>
|
||||
<span ng-show="state.actionInProgress">Deployment in progress...</span>
|
||||
</button>
|
||||
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px">{{ state.formValidationError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="settings" title-text="Advanced container settings"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<ul class="nav nav-pills nav-justified">
|
||||
<li class="active interactive"><a data-target="#command" data-toggle="tab">Command & logging</a></li>
|
||||
<li class="interactive"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
|
||||
<li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li>
|
||||
<li class="interactive"><a data-target="#env" data-toggle="tab">Env</a></li>
|
||||
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
|
||||
<li class="interactive"><a data-target="#restart-policy" data-toggle="tab">Restart policy</a></li>
|
||||
<li class="interactive"><a data-target="#runtime-resources" ng-mouseup="refreshSlider()" data-toggle="tab">Runtime & Resources</a></li>
|
||||
<li ng-if="areContainerCapabilitiesEnabled" class="interactive"><a data-target="#container-capabilities" data-toggle="tab">Capabilities</a></li>
|
||||
</ul>
|
||||
<div class="form-horizontal" ng-if="state.containerIsLoaded">
|
||||
<!-- tab-content -->
|
||||
<div class="tab-content">
|
||||
<!-- tab-command -->
|
||||
<div class="tab-pane active" id="command">
|
||||
<docker-create-container-commands-tab
|
||||
values="formValues.commands"
|
||||
api-version="applicationState.endpoint.apiVersion"
|
||||
on-change="(handleCommandsChange)"
|
||||
></docker-create-container-commands-tab>
|
||||
</div>
|
||||
<!-- !tab-command -->
|
||||
|
||||
<div class="tab-pane" id="volumes">
|
||||
<docker-create-container-volumes-tab ng-if="state.containerIsLoaded" values="formValues.volumes" on-change="(onVolumesChange)" allow-bind-mounts="allowBindMounts">
|
||||
</docker-create-container-volumes-tab>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="network">
|
||||
<docker-create-container-network-tab values="formValues.network" on-change="(onNetworkChange)"> </docker-create-container-network-tab>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="labels">
|
||||
<docker-create-container-labels-tab values="formValues.labels" on-change="(onLabelsChange)"></docker-create-container-labels-tab>
|
||||
</div>
|
||||
|
||||
<!-- tab-env -->
|
||||
<div class="tab-pane" id="env">
|
||||
<docker-create-container-env-vars-tab
|
||||
ng-if="state.containerIsLoaded"
|
||||
values="formValues.envVars"
|
||||
on-change="(handleEnvVarsChange)"
|
||||
></docker-create-container-env-vars-tab>
|
||||
</div>
|
||||
<!-- !tab-env -->
|
||||
|
||||
<div class="tab-pane" id="restart-policy">
|
||||
<docker-create-container-restart-policy-tab values="formValues.restartPolicy" on-change="(onRestartPolicyChange)"></docker-create-container-restart-policy-tab>
|
||||
</div>
|
||||
|
||||
<!-- tab-runtime-resources -->
|
||||
<div class="tab-pane" id="runtime-resources">
|
||||
<docker-create-container-resources-tab
|
||||
values="formValues.resources"
|
||||
on-change="(onResourcesChange)"
|
||||
allow-privileged-mode="allowPrivilegedMode"
|
||||
is-devices-field-visible="showDeviceMapping"
|
||||
is-sysctl-field-visible="allowSysctl"
|
||||
is-init-field-visible="applicationState.endpoint.apiVersion >= 1.37"
|
||||
is-image-invalid="!formValues.RegistryModel.Image || (!formValues.RegistryModel.Registry && fromContainer)"
|
||||
redeploy="(redeployUnlimitedResources)"
|
||||
is-duplicate="state.mode == 'duplicate'"
|
||||
validation-data="{
|
||||
maxMemory: state.sliderMaxMemory,
|
||||
maxCpu: state.sliderMaxCpu,
|
||||
}"
|
||||
></docker-create-container-resources-tab>
|
||||
</div>
|
||||
<!-- !tab-runtime-resources -->
|
||||
<!-- tab-container-capabilities -->
|
||||
<div class="tab-pane" id="container-capabilities">
|
||||
<docker-create-container-capabilities-tab values="formValues.capabilities" on-change="(onCapabilitiesChange)"></docker-create-container-capabilities-tab>
|
||||
</div>
|
||||
<!-- !tab-container-capabilities -->
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
Loading…
Add table
Add a link
Reference in a new issue