diff --git a/app/edge/react/components/index.ts b/app/edge/react/components/index.ts
index b4913e51c..6f0a432b1 100644
--- a/app/edge/react/components/index.ts
+++ b/app/edge/react/components/index.ts
@@ -4,9 +4,6 @@ import { r2a } from '@/react-tools/react2angular';
import { withReactQuery } from '@/react-tools/withReactQuery';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { AssociatedEdgeEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeEnvironmentsSelector';
-import { EdgeAsyncIntervalsForm } from '@/react/edge/components/EdgeAsyncIntervalsForm';
-import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField';
-import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
import { EdgeGroupsSelector } from '@/react/edge/edge-stacks/components/EdgeGroupsSelector';
const ngModule = angular
@@ -23,37 +20,7 @@ const ngModule = angular
'required',
])
)
- .component(
- 'edgeScriptForm',
- r2a(withReactQuery(EdgeScriptForm), [
- 'edgeInfo',
- 'commands',
- 'asyncMode',
- 'showMetaFields',
- ])
- )
- .component(
- 'edgeCheckinIntervalField',
- r2a(withReactQuery(EdgeCheckinIntervalField), [
- 'value',
- 'onChange',
- 'isDefaultHidden',
- 'tooltip',
- 'label',
- 'readonly',
- 'size',
- ])
- )
- .component(
- 'edgeAsyncIntervalsForm',
- r2a(withReactQuery(EdgeAsyncIntervalsForm), [
- 'values',
- 'onChange',
- 'isDefaultHidden',
- 'readonly',
- 'fieldSettings',
- ])
- )
+
.component(
'associatedEdgeEnvironmentsSelector',
r2a(withReactQuery(AssociatedEdgeEnvironmentsSelector), [
diff --git a/app/portainer/__module.js b/app/portainer/__module.js
index da42183eb..bf0e0adcd 100644
--- a/app/portainer/__module.js
+++ b/app/portainer/__module.js
@@ -194,8 +194,7 @@ angular
},
views: {
'content@': {
- templateUrl: './views/endpoints/edit/endpoint.html',
- controller: 'EndpointController',
+ component: 'environmentsItemView',
},
},
};
diff --git a/app/portainer/components/endpointSecurity/por-endpoint-security.js b/app/portainer/components/endpointSecurity/por-endpoint-security.js
deleted file mode 100644
index 7b9c54f35..000000000
--- a/app/portainer/components/endpointSecurity/por-endpoint-security.js
+++ /dev/null
@@ -1,12 +0,0 @@
-angular.module('portainer.app').component('porEndpointSecurity', {
- templateUrl: './porEndpointSecurity.html',
- controller: 'porEndpointSecurityController',
- bindings: {
- // This object will be populated with the form data.
- // Model reference in endpointSecurityModel.js
- formData: '=',
- // The component will use this object to initialize the default values
- // if present.
- endpoint: '<',
- },
-});
diff --git a/app/portainer/components/endpointSecurity/porEndpointSecurity.html b/app/portainer/components/endpointSecurity/porEndpointSecurity.html
deleted file mode 100644
index b76955bd4..000000000
--- a/app/portainer/components/endpointSecurity/porEndpointSecurity.html
+++ /dev/null
@@ -1,83 +0,0 @@
-
-
-
-
-
TLS mode
-
-
-
-
-
-
Required TLS files
-
-
-
-
diff --git a/app/portainer/components/endpointSecurity/porEndpointSecurityController.js b/app/portainer/components/endpointSecurity/porEndpointSecurityController.js
deleted file mode 100644
index 56d11a562..000000000
--- a/app/portainer/components/endpointSecurity/porEndpointSecurityController.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import { tlsOptions } from '@/react/portainer/environments/ItemView/tls-options';
-
-angular.module('portainer.app').controller('porEndpointSecurityController', [
- '$scope',
- function ($scope) {
- var ctrl = this;
-
- this.tlsOptions = tlsOptions;
-
- function onChange(values) {
- $scope.$evalAsync(() => {
- ctrl.formData = {
- ...ctrl.formData,
- ...values,
- };
- });
- }
-
- ctrl.onChangeTLSMode = onChangeTLSMode;
- function onChangeTLSMode(mode) {
- onChange({ TLSMode: mode });
- }
-
- ctrl.onToggleTLS = onToggleTLS;
- function onToggleTLS(newValue) {
- onChange({ TLS: newValue });
- }
-
- this.$onInit = $onInit;
- function $onInit() {
- if (ctrl.endpoint) {
- var endpoint = ctrl.endpoint;
- var TLS = endpoint.TLSConfig.TLS;
- ctrl.formData.TLS = TLS;
- var CACert = endpoint.TLSConfig.TLSCACert;
- ctrl.formData.TLSCACert = CACert;
- var cert = endpoint.TLSConfig.TLSCert;
- ctrl.formData.TLSCert = cert;
- var key = endpoint.TLSConfig.TLSKey;
- ctrl.formData.TLSKey = key;
-
- if (TLS) {
- if (CACert && cert && key) {
- ctrl.formData.TLSMode = 'tls_client_ca';
- } else if (cert && key) {
- ctrl.formData.TLSMode = 'tls_client_noca';
- } else if (CACert) {
- ctrl.formData.TLSMode = 'tls_ca';
- } else {
- ctrl.formData.TLSMode = 'tls_only';
- }
- }
- }
- }
- },
-]);
diff --git a/app/portainer/components/endpointSecurity/porEndpointSecurityModel.js b/app/portainer/components/endpointSecurity/porEndpointSecurityModel.js
deleted file mode 100644
index 795bdb754..000000000
--- a/app/portainer/components/endpointSecurity/porEndpointSecurityModel.js
+++ /dev/null
@@ -1,7 +0,0 @@
-export function EndpointSecurityFormData() {
- this.TLS = false;
- this.TLSMode = 'tls_client_ca';
- this.TLSCACert = null;
- this.TLSCert = null;
- this.TLSKey = null;
-}
diff --git a/app/portainer/components/index.js b/app/portainer/components/index.js
index 033af9f17..fb0f89632 100644
--- a/app/portainer/components/index.js
+++ b/app/portainer/components/index.js
@@ -8,9 +8,8 @@ import { boxSelectorModule } from './BoxSelector';
import { beFeatureIndicator } from './BEFeatureIndicator';
import { InformationPanelAngular } from './InformationPanel';
import { gitFormModule } from './forms/git-form';
-import { tlsFieldsetModule } from './tls-fieldset';
export default angular
- .module('portainer.app.components', [boxSelectorModule, widgetModule, gitFormModule, porAccessManagementModule, formComponentsModule, tlsFieldsetModule])
+ .module('portainer.app.components', [boxSelectorModule, widgetModule, gitFormModule, porAccessManagementModule, formComponentsModule])
.component('informationPanel', InformationPanelAngular)
.component('beFeatureIndicator', beFeatureIndicator).name;
diff --git a/app/portainer/components/tls-fieldset/index.ts b/app/portainer/components/tls-fieldset/index.ts
deleted file mode 100644
index 32ab1f820..000000000
--- a/app/portainer/components/tls-fieldset/index.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import angular from 'angular';
-
-import {
- TLSFieldset,
- tlsConfigValidation,
-} from '@/react/components/TLSFieldset';
-import { withFormValidation } from '@/react-tools/withFormValidation';
-
-export const ngModule = angular.module(
- 'portainer.app.components.tls-fieldset',
- []
-);
-
-export const tlsFieldsetModule = ngModule.name;
-
-withFormValidation(
- ngModule,
- TLSFieldset,
- 'tlsFieldset',
- [],
- tlsConfigValidation
-);
diff --git a/app/portainer/environments/azure-endpoint-config/azure-endpoint-config.js b/app/portainer/environments/azure-endpoint-config/azure-endpoint-config.js
deleted file mode 100644
index 02ab69e04..000000000
--- a/app/portainer/environments/azure-endpoint-config/azure-endpoint-config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export const azureEndpointConfig = {
- bindings: {
- applicationId: '=',
- tenantId: '=',
- authenticationKey: '=',
- },
- templateUrl: './azureEndpointConfig.html',
-};
diff --git a/app/portainer/environments/azure-endpoint-config/azureEndpointConfig.html b/app/portainer/environments/azure-endpoint-config/azureEndpointConfig.html
deleted file mode 100644
index 585296a09..000000000
--- a/app/portainer/environments/azure-endpoint-config/azureEndpointConfig.html
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
Azure configuration
-
-
-
-
-
-
-
-
-
-
diff --git a/app/portainer/environments/index.ts b/app/portainer/environments/index.ts
index 0efeac57a..d454599ef 100644
--- a/app/portainer/environments/index.ts
+++ b/app/portainer/environments/index.ts
@@ -1,7 +1,3 @@
import angular from 'angular';
-import { azureEndpointConfig } from './azure-endpoint-config/azure-endpoint-config';
-
-export default angular
- .module('portainer.environments', [])
- .component('azureEndpointConfig', azureEndpointConfig).name;
+export default angular.module('portainer.environments', []).name;
diff --git a/app/portainer/hostmanagement/open-amt/open-amt.service.ts b/app/portainer/hostmanagement/open-amt/open-amt.service.ts
index 64591b46f..e9f73ed07 100644
--- a/app/portainer/hostmanagement/open-amt/open-amt.service.ts
+++ b/app/portainer/hostmanagement/open-amt/open-amt.service.ts
@@ -2,7 +2,6 @@ import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import {
OpenAMTConfiguration,
- AMTInformation,
AuthorizationResponse,
DeviceFeatures,
} from '@/react/edge/edge-devices/open-amt/types';
@@ -17,21 +16,6 @@ export async function configureAMT(formValues: OpenAMTConfiguration) {
}
}
-export async function getAMTInfo(environmentId: EnvironmentId) {
- try {
- const { data: amtInformation } = await axios.get(
- `${BASE_URL}/${environmentId}/info`
- );
-
- return amtInformation;
- } catch (e) {
- throw parseAxiosError(
- e as Error,
- 'Unable to retrieve environment information'
- );
- }
-}
-
export async function enableDeviceFeatures(
environmentId: EnvironmentId,
deviceGUID: string,
diff --git a/app/portainer/react/components/environments.ts b/app/portainer/react/components/environments.ts
index 4c5a95c7b..c43734170 100644
--- a/app/portainer/react/components/environments.ts
+++ b/app/portainer/react/components/environments.ts
@@ -1,12 +1,13 @@
import angular from 'angular';
import { r2a } from '@/react-tools/react2angular';
-import { EdgeKeyDisplay } from '@/react/portainer/environments/ItemView/EdgeKeyDisplay';
import { KVMControl } from '@/react/portainer/environments/KvmView/KVMControl';
import { TagsDatatable } from '@/react/portainer/environments/TagsView/TagsDatatable';
export const environmentsModule = angular
.module('portainer.app.react.components.environments', [])
- .component('edgeKeyDisplay', r2a(EdgeKeyDisplay, ['edgeKey']))
- .component('kvmControl', r2a(KVMControl, ['deviceId', 'server', 'token']))
- .component('tagsDatatable', r2a(TagsDatatable, ['dataset', 'onRemove'])).name;
+ .component('tagsDatatable', r2a(TagsDatatable, ['dataset', 'onRemove']))
+ .component(
+ 'kvmControl',
+ r2a(KVMControl, ['deviceId', 'server', 'token'])
+ ).name;
diff --git a/app/portainer/react/views/environments.ts b/app/portainer/react/views/environments.ts
new file mode 100644
index 000000000..eaf6227e3
--- /dev/null
+++ b/app/portainer/react/views/environments.ts
@@ -0,0 +1,19 @@
+import angular from 'angular';
+
+import { ListView } from '@/react/portainer/environments/ListView';
+import { r2a } from '@/react-tools/react2angular';
+import { withCurrentUser } from '@/react-tools/withCurrentUser';
+import { withReactQuery } from '@/react-tools/withReactQuery';
+import { withUIRouter } from '@/react-tools/withUIRouter';
+import { ItemView } from '@/react/portainer/environments/ItemView/ItemView';
+
+export const environmentsModule = angular
+ .module('portainer.app.environments', [])
+ .component(
+ 'environmentsItemView',
+ r2a(withUIRouter(withReactQuery(withCurrentUser(ItemView))), [])
+ )
+ .component(
+ 'environmentsListView',
+ r2a(withUIRouter(withReactQuery(withCurrentUser(ListView))), [])
+ ).name;
diff --git a/app/portainer/react/views/index.ts b/app/portainer/react/views/index.ts
index 52c994926..c417817e8 100644
--- a/app/portainer/react/views/index.ts
+++ b/app/portainer/react/views/index.ts
@@ -8,7 +8,6 @@ import { withUIRouter } from '@/react-tools/withUIRouter';
import { CreateUserAccessToken } from '@/react/portainer/account/CreateAccessTokenView';
import { EdgeComputeSettingsView } from '@/react/portainer/settings/EdgeComputeView/EdgeComputeSettingsView';
import { EdgeAutoCreateScriptView } from '@/react/portainer/environments/EdgeAutoCreateScriptView';
-import { ListView as EnvironmentsListView } from '@/react/portainer/environments/ListView';
import { BackupSettingsPanel } from '@/react/portainer/settings/SettingsView/BackupSettingsView/BackupSettingsPanel';
import { SettingsView } from '@/react/portainer/settings/SettingsView/SettingsView';
import { CreateHelmRepositoriesView } from '@/react/portainer/account/helm-repositories/CreateHelmRepositoryView';
@@ -20,6 +19,7 @@ import { environmentGroupModule } from './env-groups';
import { registriesModule } from './registries';
import { activityLogsModule } from './activity-logs';
import { templatesModule } from './templates';
+import { environmentsModule } from './environments';
export const viewsModule = angular
.module('portainer.app.react.views', [
@@ -30,6 +30,7 @@ export const viewsModule = angular
registriesModule,
activityLogsModule,
templatesModule,
+ environmentsModule,
])
.component(
'homeView',
@@ -56,10 +57,6 @@ export const viewsModule = angular
['onSubmit', 'settings']
)
)
- .component(
- 'environmentsListView',
- r2a(withUIRouter(withReactQuery(withCurrentUser(EnvironmentsListView))), [])
- )
.component(
'backupSettingsPanel',
r2a(withUIRouter(withReactQuery(withCurrentUser(BackupSettingsPanel))), [])
diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html
deleted file mode 100644
index 3fcdc4d9a..000000000
--- a/app/portainer/views/endpoints/edit/endpoint.html
+++ /dev/null
@@ -1,268 +0,0 @@
-
-
-
-
-
-
-
-
- This Edge environment is associated to an Edge environment {{ state.kubernetesEndpoint ? '(Kubernetes)' : '(Docker)' }}.
-
-
- Edge key: {{ endpoint.EdgeKey }}
-
-
- Edge identifier: {{ endpoint.EdgeID }}
-
-
-
-
-
-
-
-
-
-
- Deploy an agent
-
-
-
- Refer to the platform related command below to deploy the Edge agent in your remote cluster.
-
-
- The agent will communicate with Portainer via {{ edgeKeyDetails.instanceURL }} and tcp://{{ edgeKeyDetails.tunnelServerAddr }}
-
-
-
- Edge agent deployment script
-
-
-
-
-
-
-
-
-
-
-
- You should configure the features available in this Kubernetes environment in the
- Kubernetes configuration view.
-
-
-
-
-
diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js
deleted file mode 100644
index 4b8d3d585..000000000
--- a/app/portainer/views/endpoints/edit/endpointController.js
+++ /dev/null
@@ -1,372 +0,0 @@
-import _ from 'lodash-es';
-import uuidv4 from 'uuid/v4';
-
-import { PortainerEndpointTypes } from '@/portainer/models/endpoint/models';
-import EndpointHelper from '@/portainer/helpers/endpointHelper';
-import { getAMTInfo } from 'Portainer/hostmanagement/open-amt/open-amt.service';
-import { confirmDestructive } from '@@/modals/confirm';
-import { isEdgeEnvironment, isDockerAPIEnvironment } from '@/react/portainer/environments/utils';
-
-import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
-import { confirmDisassociate } from '@/react/portainer/environments/ItemView/ConfirmDisassociateModel';
-import { buildConfirmButton } from '@@/modals/utils';
-import { getInfo } from '@/react/docker/proxy/queries/useInfo';
-
-angular.module('portainer.app').controller('EndpointController', EndpointController);
-
-/* @ngInject */
-function EndpointController(
- $async,
- $scope,
- $state,
- $transition$,
- $filter,
- clipboard,
- EndpointService,
- GroupService,
-
- Notifications,
- Authentication,
- SettingsService
-) {
- $scope.onChangeCheckInInterval = onChangeCheckInInterval;
- $scope.setFieldValue = setFieldValue;
- $scope.onChangeTags = onChangeTags;
- $scope.onChangeTLSConfigFormValues = onChangeTLSConfigFormValues;
-
- $scope.state = {
- selectAll: false,
- // displayTextFilter: false,
- get selectedItemCount() {
- return $scope.state.selectedItems.length || 0;
- },
- selectedItems: [],
- uploadInProgress: false,
- actionInProgress: false,
- azureEndpoint: false,
- kubernetesEndpoint: false,
- agentEndpoint: false,
- edgeEndpoint: false,
- edgeAssociated: false,
- allowCreate: Authentication.isAdmin(),
- allowSelfSignedCerts: true,
- showAMTInfo: false,
- showTLSConfig: false,
- edgeScriptCommands: {
- linux: _.compact([commandsTabs.k8sLinux, commandsTabs.swarmLinux, commandsTabs.standaloneLinux]),
- win: [commandsTabs.swarmWindows, commandsTabs.standaloneWindow],
- },
- };
-
- $scope.selectAll = function () {
- $scope.state.firstClickedItem = null;
- for (var i = 0; i < $scope.state.filteredDataSet.length; i++) {
- var item = $scope.state.filteredDataSet[i];
- if (item.Checked !== $scope.state.selectAll) {
- // if ($scope.allowSelection(item) && item.Checked !== $scope.state.selectAll) {
- item.Checked = $scope.state.selectAll;
- $scope.selectItem(item);
- }
- }
- };
-
- function isBetween(value, a, b) {
- return (value >= a && value <= b) || (value >= b && value <= a);
- }
-
- $scope.selectItem = function (item, event) {
- // Handle range select using shift
- if (event && event.originalEvent.shiftKey && $scope.state.firstClickedItem) {
- const firstItemIndex = $scope.state.filteredDataSet.indexOf($scope.state.firstClickedItem);
- const lastItemIndex = $scope.state.filteredDataSet.indexOf(item);
- const itemsInRange = _.filter($scope.state.filteredDataSet, (item, index) => {
- return isBetween(index, firstItemIndex, lastItemIndex);
- });
- const value = $scope.state.firstClickedItem.Checked;
-
- _.forEach(itemsInRange, (i) => {
- i.Checked = value;
- });
- $scope.state.firstClickedItem = item;
- } else if (event) {
- item.Checked = !item.Checked;
- $scope.state.firstClickedItem = item;
- }
- $scope.state.selectedItems = _.uniq(_.concat($scope.state.selectedItems, $scope.state.filteredDataSet)).filter((i) => i.Checked);
- if (event && $scope.state.selectAll && $scope.state.selectedItems.length !== $scope.state.filteredDataSet.length) {
- $scope.state.selectAll = false;
- }
- };
-
- $scope.formValues = {
- tlsConfig: {
- tls: false,
- skipVerify: false,
- skipClientVerify: false,
- caCertFile: null,
- certFile: null,
- keyFile: null,
- },
- };
-
- $scope.onDisassociateEndpoint = async function () {
- confirmDisassociate().then((confirmed) => {
- if (confirmed) {
- disassociateEndpoint();
- }
- });
- };
-
- async function disassociateEndpoint() {
- var endpoint = $scope.endpoint;
-
- try {
- $scope.state.actionInProgress = true;
- await EndpointService.disassociateEndpoint(endpoint.Id);
- Notifications.success('Environment disassociated', $scope.endpoint.Name);
- $state.reload();
- } catch (err) {
- Notifications.error('Failure', err, 'Unable to disassociate environment');
- } finally {
- $scope.state.actionInProgress = false;
- }
- }
-
- function onChangeCheckInInterval(value) {
- setFieldValue('EdgeCheckinInterval', value);
- }
-
- function onChangeTags(value) {
- setFieldValue('TagIds', value);
- }
-
- function onChangeTLSConfigFormValues(newValues) {
- return this.$async(async () => {
- $scope.formValues.tlsConfig = {
- ...$scope.formValues.tlsConfig,
- ...newValues,
- };
- });
- }
-
- function setFieldValue(name, value) {
- return $scope.$evalAsync(() => {
- $scope.endpoint = {
- ...$scope.endpoint,
- [name]: value,
- };
- });
- }
-
- Array.prototype.indexOf = function (val) {
- for (var i = 0; i < this.length; i++) {
- if (this[i] == val) return i;
- }
- return -1;
- };
- Array.prototype.remove = function (val) {
- var index = this.indexOf(val);
- if (index > -1) {
- this.splice(index, 1);
- }
- };
-
- $scope.updateEndpoint = async function () {
- var endpoint = $scope.endpoint;
-
- if (isEdgeEnvironment(endpoint.Type) && _.difference($scope.initialTagIds, endpoint.TagIds).length > 0) {
- let confirmed = await confirmDestructive({
- title: 'Confirm action',
- message: 'Removing tags from this environment will remove the corresponding edge stacks when dynamic grouping is being used',
- confirmButton: buildConfirmButton(),
- });
-
- if (!confirmed) {
- return;
- }
- }
-
- var payload = {
- Name: endpoint.Name,
- PublicURL: endpoint.PublicURL,
- Gpus: endpoint.Gpus,
- GroupID: endpoint.GroupId,
- TagIds: endpoint.TagIds,
- AzureApplicationID: endpoint.AzureCredentials.ApplicationID,
- AzureTenantID: endpoint.AzureCredentials.TenantID,
- AzureAuthenticationKey: endpoint.AzureCredentials.AuthenticationKey,
- EdgeCheckinInterval: endpoint.EdgeCheckinInterval,
- };
-
- if (
- $scope.endpointType !== 'local' &&
- endpoint.Type !== PortainerEndpointTypes.AzureEnvironment &&
- endpoint.Type !== PortainerEndpointTypes.KubernetesLocalEnvironment &&
- endpoint.Type !== PortainerEndpointTypes.AgentOnKubernetesEnvironment
- ) {
- payload.URL = 'tcp://' + endpoint.URL;
-
- if (endpoint.Type === PortainerEndpointTypes.DockerEnvironment) {
- var tlsConfig = $scope.formValues.tlsConfig;
- payload.TLS = tlsConfig.tls;
- payload.TLSSkipVerify = tlsConfig.skipVerify;
- if (tlsConfig.tls && !tlsConfig.skipVerify) {
- payload.TLSSkipClientVerify = tlsConfig.skipClientVerify;
- payload.TLSCACert = tlsConfig.caCertFile;
- payload.TLSCert = tlsConfig.certFile;
- payload.TLSKey = tlsConfig.keyFile;
- }
- }
- }
-
- if (endpoint.Type === PortainerEndpointTypes.AgentOnKubernetesEnvironment) {
- payload.URL = endpoint.URL;
- }
-
- if (endpoint.Type === PortainerEndpointTypes.KubernetesLocalEnvironment) {
- payload.URL = 'https://' + endpoint.URL;
- }
-
- $scope.state.actionInProgress = true;
- EndpointService.updateEndpoint(endpoint.Id, payload).then(
- function success() {
- Notifications.success('Environment updated', $scope.endpoint.Name);
- $state.go($state.params.redirectTo || 'portainer.endpoints', {}, { reload: true });
- },
- function error(err) {
- Notifications.error('Failure', err, 'Unable to update environment');
- $scope.state.actionInProgress = false;
- },
- function update(evt) {
- if (evt.upload) {
- $scope.state.uploadInProgress = evt.upload;
- }
- }
- );
- };
-
- function decodeEdgeKey(key) {
- let keyInformation = {};
-
- if (key === '') {
- return keyInformation;
- }
-
- let decodedKey = _.split(atob(key), '|');
- keyInformation.instanceURL = decodedKey[0];
- keyInformation.tunnelServerAddr = decodedKey[1];
-
- return keyInformation;
- }
-
- function configureState() {
- if (
- $scope.endpoint.Type === PortainerEndpointTypes.KubernetesLocalEnvironment ||
- $scope.endpoint.Type === PortainerEndpointTypes.AgentOnKubernetesEnvironment ||
- $scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment
- ) {
- $scope.state.kubernetesEndpoint = true;
- }
- if ($scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || $scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
- $scope.state.edgeEndpoint = true;
- }
- if ($scope.endpoint.Type === PortainerEndpointTypes.AzureEnvironment) {
- $scope.state.azureEndpoint = true;
- }
- if (
- $scope.endpoint.Type === PortainerEndpointTypes.AgentOnDockerEnvironment ||
- $scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment ||
- $scope.endpoint.Type === PortainerEndpointTypes.AgentOnKubernetesEnvironment ||
- $scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment
- ) {
- $scope.state.agentEndpoint = true;
- }
- }
-
- function configureTLS(endpoint) {
- $scope.formValues = {
- tlsConfig: {
- tls: endpoint.TLSConfig.TLS || false,
- skipVerify: endpoint.TLSConfig.TLSSkipVerify || false,
- skipClientVerify: endpoint.TLSConfig.TLSSkipClientVerify || false,
- },
- };
- }
-
- async function initView() {
- return $async(async () => {
- try {
- const [endpoint, groups, settings] = await Promise.all([EndpointService.endpoint($transition$.params().id), GroupService.groups(), SettingsService.settings()]);
-
- if (isDockerAPIEnvironment(endpoint)) {
- $scope.state.showTLSConfig = true;
- }
-
- // Check if the environment is docker standalone, to decide whether to show the GPU insights box
- const isDockerEnvironment = endpoint.Type === PortainerEndpointTypes.DockerEnvironment;
- if (isDockerEnvironment) {
- try {
- const dockerInfo = await getInfo(endpoint.Id);
- const isDockerSwarmEnv = dockerInfo.Swarm && dockerInfo.Swarm.NodeID;
- $scope.isDockerStandaloneEnv = !isDockerSwarmEnv;
- } catch (err) {
- // $scope.isDockerStandaloneEnv is only used to show the "GPU insights box", so fail quietly on error
- }
- }
-
- if (endpoint.URL.indexOf('unix://') === 0 || endpoint.URL.indexOf('npipe://') === 0) {
- $scope.endpointType = 'local';
- } else {
- $scope.endpointType = 'remote';
- }
-
- endpoint.URL = $filter('stripprotocol')(endpoint.URL);
-
- if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
- $scope.edgeKeyDetails = decodeEdgeKey(endpoint.EdgeKey);
-
- $scope.state.edgeAssociated = !!endpoint.EdgeID;
- endpoint.EdgeID = endpoint.EdgeID || uuidv4();
- }
-
- $scope.endpoint = endpoint;
- $scope.initialTagIds = endpoint.TagIds.slice();
- $scope.groups = groups;
-
- configureState();
-
- configureTLS(endpoint);
-
- if (EndpointHelper.isDockerEndpoint(endpoint) && $scope.state.edgeAssociated) {
- $scope.state.showAMTInfo = settings && settings.openAMTConfiguration && settings.openAMTConfiguration.enabled;
- }
- } catch (err) {
- Notifications.error('Failure', err, 'Unable to retrieve environment details');
- }
-
- if ($scope.state.showAMTInfo) {
- try {
- $scope.endpoint.ManagementInfo = {};
- const amtInfo = await getAMTInfo($state.params.id);
- try {
- $scope.endpoint.ManagementInfo = JSON.parse(amtInfo.RawOutput);
- } catch (err) {
- clearAMTManagementInfo(amtInfo.RawOutput);
- }
- } catch (err) {
- clearAMTManagementInfo('Unable to retrieve AMT environment details');
- }
- }
- });
- }
-
- function clearAMTManagementInfo(versionValue) {
- $scope.endpoint.ManagementInfo['AMT'] = versionValue;
- $scope.endpoint.ManagementInfo['UUID'] = '-';
- $scope.endpoint.ManagementInfo['Control Mode'] = '-';
- $scope.endpoint.ManagementInfo['Build Number'] = '-';
- $scope.endpoint.ManagementInfo['DNS Suffix'] = '-';
- }
-
- initView();
-}
diff --git a/app/react-tools/test-mocks.ts b/app/react-tools/test-mocks.ts
index e20bf19e9..135a1e3f8 100644
--- a/app/react-tools/test-mocks.ts
+++ b/app/react-tools/test-mocks.ts
@@ -111,6 +111,7 @@ export function createMockEnvironment(): Environment {
Gpus: [],
Agent: { Version: '1.0.0' },
EnableImageNotification: false,
+ CloudProvider: undefined,
ChangeWindow: {
Enabled: false,
EndTime: '',
@@ -120,5 +121,18 @@ export function createMockEnvironment(): Environment {
detail: '',
summary: '',
},
- };
+ PublicURL: '',
+ ComposeSyntaxMaxVersion: '1',
+ TLSConfig: {
+ TLS: false,
+ TLSSkipVerify: false,
+ },
+ UserAccessPolicies: {},
+ TeamAccessPolicies: {},
+ LastCheckInDate: 0,
+ EdgeCheckinInterval: 0,
+ Heartbeat: true,
+ QueryDate: 0,
+ LocalTimeZone: '',
+ } satisfies Environment;
}
diff --git a/app/react/components/form-components/formikUtils.ts b/app/react/components/form-components/formikUtils.ts
index 459b0259c..90634ad06 100644
--- a/app/react/components/form-components/formikUtils.ts
+++ b/app/react/components/form-components/formikUtils.ts
@@ -1,4 +1,5 @@
import { FormikErrors } from 'formik';
+import { SetStateAction } from 'react';
export function isErrorType(
error: string | FormikErrors | undefined
@@ -16,3 +17,17 @@ export function isArrayErrorType(
): error is FormikErrors[] {
return error !== undefined && typeof error !== 'string';
}
+
+export interface FieldsetValues {
+ values: TFieldset;
+ errors?: FormikErrors;
+}
+
+export type SetFieldValue = (
+ field: keyof TFieldset,
+ value: TField
+) => void;
+
+export type SetValues = SetStateAction;
+
+export type OnChange = (value: TFieldset) => void;
diff --git a/app/react/docker/containers/CreateView/ResourcesTab/GpuFieldset/GpuFieldset.tsx b/app/react/docker/containers/CreateView/ResourcesTab/GpuFieldset/GpuFieldset.tsx
index 09d1fada8..2ab6e1006 100644
--- a/app/react/docker/containers/CreateView/ResourcesTab/GpuFieldset/GpuFieldset.tsx
+++ b/app/react/docker/containers/CreateView/ResourcesTab/GpuFieldset/GpuFieldset.tsx
@@ -7,6 +7,8 @@ import {
} from 'react-select/dist/declarations/src/types';
import { OptionProps } from 'react-select/dist/declarations/src/components/Option';
+import { Pair } from '@/react/portainer/settings/types';
+
import { Select } from '@@/form-components/ReactSelect';
import { Switch } from '@@/form-components/SwitchField/Switch';
import { Tooltip } from '@@/Tip/Tooltip';
@@ -20,15 +22,10 @@ interface GpuOption {
description?: string;
}
-interface GPU {
- value: string;
- name: string;
-}
-
export interface Props {
values: Values;
onChange(values: Values): void;
- gpus: GPU[];
+ gpus: Pair[];
usedGpus: string[];
usedAllGpus: boolean;
enableGpuManagement?: boolean;
@@ -77,13 +74,15 @@ export function GpuFieldset({
enableGpuManagement,
}: Props) {
const options = useMemo(() => {
- const options = (gpus || []).map((gpu) => ({
- value: gpu.value,
- label:
- usedGpus.includes(gpu.value) || usedAllGpus
- ? `${gpu.name} (in use)`
- : gpu.name,
- }));
+ const options = (gpus || [])
+ .filter((gpu): gpu is { value: string; name: string } => !!gpu.value)
+ .map((gpu) => ({
+ value: gpu.value,
+ label:
+ usedGpus.includes(gpu.value) || usedAllGpus
+ ? `${gpu.name} (in use)`
+ : gpu.name,
+ }));
options.unshift({
value: 'all',
diff --git a/app/react/edge/components/EdgeScriptForm/EdgeScriptSettingsFieldset.tsx b/app/react/edge/components/EdgeScriptForm/EdgeScriptSettingsFieldset.tsx
index 3aed3f6bd..9843bdd41 100644
--- a/app/react/edge/components/EdgeScriptForm/EdgeScriptSettingsFieldset.tsx
+++ b/app/react/edge/components/EdgeScriptForm/EdgeScriptSettingsFieldset.tsx
@@ -1,6 +1,6 @@
import { useFormikContext, Field } from 'formik';
-import { GroupField } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/GroupsField';
+import { GroupField } from '@/react/portainer/environments/common/MetadataFieldset/GroupsField';
import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
diff --git a/app/react/edge/edge-devices/open-amt/query-keys.ts b/app/react/edge/edge-devices/open-amt/query-keys.ts
new file mode 100644
index 000000000..ebd065740
--- /dev/null
+++ b/app/react/edge/edge-devices/open-amt/query-keys.ts
@@ -0,0 +1,9 @@
+import { EnvironmentId } from '@/react/portainer/environments/types';
+
+export const queryKeys = {
+ base: (environmentId: EnvironmentId) => ['open-amt', environmentId] as const,
+ devices: (environmentId: EnvironmentId) =>
+ [...queryKeys.base(environmentId), 'devices'] as const,
+ info: (environmentId: EnvironmentId) =>
+ [...queryKeys.base(environmentId), 'info'] as const,
+};
diff --git a/app/react/edge/edge-devices/open-amt/types.ts b/app/react/edge/edge-devices/open-amt/types.ts
index 59a3d9951..47e326575 100644
--- a/app/react/edge/edge-devices/open-amt/types.ts
+++ b/app/react/edge/edge-devices/open-amt/types.ts
@@ -9,15 +9,6 @@ export interface OpenAMTConfiguration {
certFilePassword: string;
}
-export interface AMTInformation {
- uuid: string;
- amt: string;
- buildNumber: string;
- controlMode: string;
- dnsSuffix: string;
- rawOutput: string;
-}
-
export interface AuthorizationResponse {
server: string;
token: string;
diff --git a/app/react/edge/edge-devices/open-amt/useAMTDevices.tsx b/app/react/edge/edge-devices/open-amt/useAMTDevices.tsx
index d23e3b375..58ca62613 100644
--- a/app/react/edge/edge-devices/open-amt/useAMTDevices.tsx
+++ b/app/react/edge/edge-devices/open-amt/useAMTDevices.tsx
@@ -5,13 +5,14 @@ import { withError } from '@/react-tools/react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { Device } from './types';
+import { queryKeys } from './query-keys';
export function useAMTDevices(
environmentId: EnvironmentId,
{ enabled }: { enabled?: boolean } = {}
) {
return useQuery(
- ['amt_devices', environmentId],
+ queryKeys.devices(environmentId),
() => getDevices(environmentId),
{
...withError('Failed retrieving AMT devices'),
diff --git a/app/react/edge/edge-devices/open-amt/useAmtInfo.ts b/app/react/edge/edge-devices/open-amt/useAmtInfo.ts
new file mode 100644
index 000000000..d58c1928e
--- /dev/null
+++ b/app/react/edge/edge-devices/open-amt/useAmtInfo.ts
@@ -0,0 +1,38 @@
+import { useQuery } from 'react-query';
+
+import axios, { parseAxiosError } from '@/portainer/services/axios';
+import { EnvironmentId } from '@/react/portainer/environments/types';
+
+import { queryKeys } from './query-keys';
+
+interface AMTInformation {
+ uuid: string;
+ amt: string;
+ buildNumber: string;
+ controlMode: string;
+ dnsSuffix: string;
+ rawOutput: string;
+}
+
+export function useAMTInfo(
+ environmentId: EnvironmentId,
+ { enabled = true } = {}
+) {
+ return useQuery({
+ queryKey: queryKeys.info(environmentId),
+ queryFn: () => getAMTInfo(environmentId),
+ enabled,
+ });
+}
+
+async function getAMTInfo(environmentId: EnvironmentId) {
+ try {
+ const { data: amtInformation } = await axios.get(
+ `/open_amt/${environmentId}/info`
+ );
+
+ return amtInformation;
+ } catch (e) {
+ throw parseAxiosError(e, 'Unable to retrieve environment information');
+ }
+}
diff --git a/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/EdgeStacksStatus.tsx b/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/EdgeStacksStatus.tsx
index ef4661b6d..53904075a 100644
--- a/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/EdgeStacksStatus.tsx
+++ b/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/EdgeStacksStatus.tsx
@@ -26,8 +26,8 @@ export function EdgeStackStatus({ edgeStack }: { edgeStack: EdgeStack }) {
return null;
}
- const hasOldVersion = environmentsQuery.environments.some((env) =>
- isVersionSmaller(env.Agent.Version, '2.19.0')
+ const hasOldVersion = environmentsQuery.environments.some(
+ (env) => !env.Agent.Version || isVersionSmaller(env.Agent.Version, '2.19.0')
);
const { icon, label, mode, spin, tooltip } = getStatus(
diff --git a/app/react/kubernetes/cluster/ConfigureView/ConfigureForm/ConfigureForm.tsx b/app/react/kubernetes/cluster/ConfigureView/ConfigureForm/ConfigureForm.tsx
index 61a053ed1..56e70b332 100644
--- a/app/react/kubernetes/cluster/ConfigureView/ConfigureForm/ConfigureForm.tsx
+++ b/app/react/kubernetes/cluster/ConfigureView/ConfigureForm/ConfigureForm.tsx
@@ -461,6 +461,7 @@ function useInitialValues(
ingressClasses:
getIngressClassesFormValues(allowNoneIngressClass, ingressClasses) ||
[],
+ changeWindow: environment.ChangeWindow,
};
}, [environment, ingressClasses, storageClassFormValues]);
}
diff --git a/app/react/kubernetes/cluster/ConfigureView/ConfigureForm/types.ts b/app/react/kubernetes/cluster/ConfigureView/ConfigureForm/types.ts
index 5572845ef..a38d5d1c0 100644
--- a/app/react/kubernetes/cluster/ConfigureView/ConfigureForm/types.ts
+++ b/app/react/kubernetes/cluster/ConfigureView/ConfigureForm/types.ts
@@ -1,3 +1,8 @@
+import {
+ DeploymentOptions,
+ EndpointChangeWindow,
+} from '@/react/portainer/environments/types';
+
import { IngressControllerClassMap } from '../../ingressClass/types';
export type AccessMode = {
@@ -24,5 +29,8 @@ export type ConfigureFormValues = {
ingressAvailabilityPerNamespace: boolean;
allowNoneIngressClass: boolean;
storageClasses: StorageClassFormValues[];
+ deploymentOptions?: DeploymentOptions;
+ changeWindow: EndpointChangeWindow;
+ timeZone?: string;
ingressClasses: IngressControllerClassMap[];
};
diff --git a/app/react/kubernetes/cluster/ConfigureView/ConfigureForm/validation.ts b/app/react/kubernetes/cluster/ConfigureView/ConfigureForm/validation.ts
index 8313118e3..74079d97e 100644
--- a/app/react/kubernetes/cluster/ConfigureView/ConfigureForm/validation.ts
+++ b/app/react/kubernetes/cluster/ConfigureView/ConfigureForm/validation.ts
@@ -6,6 +6,13 @@ import { IngressControllerClassMap } from '../../ingressClass/types';
import { ConfigureFormValues } from './types';
+const deploymentOptionsSchema = object().shape({
+ overrideGlobalOptions: boolean(),
+ hideAddWithForm: boolean(),
+ hideWebEditor: boolean(),
+ hideFileUpload: boolean(),
+});
+
// Define Yup schema for AccessMode
const accessModeSchema = object().shape({
Description: string().required(),
@@ -77,4 +84,6 @@ export const configureValidationSchema: SchemaOf = object({
changeWindow: isBE ? endpointChangeWindowSchema.required() : undefined,
storageClasses: storageClassFormValuesSchema.required(),
ingressClasses: array().of(ingressControllerClassMapSchema).required(),
+ timeZone: string(),
+ deploymentOptions: deploymentOptionsSchema.nullable(),
});
diff --git a/app/react/kubernetes/cluster/microk8s/addons/addons.service.ts b/app/react/kubernetes/cluster/microk8s/addons/addons.service.ts
new file mode 100644
index 000000000..378701b80
--- /dev/null
+++ b/app/react/kubernetes/cluster/microk8s/addons/addons.service.ts
@@ -0,0 +1,113 @@
+import { useMutation, useQuery } from 'react-query';
+
+import axios, { parseAxiosError } from '@/portainer/services/axios';
+import { withError } from '@/react-tools/react-query';
+import { EnvironmentStatus } from '@/react/portainer/environments/types';
+
+import { Option } from '@@/form-components/Input/Select';
+
+import { AddOnFormValue } from './types';
+
+export interface AddonsResponse {
+ microk8s: {
+ running: boolean;
+ };
+ highAvailability: {
+ enabled: boolean;
+ nodes: {
+ address: string;
+ role: string;
+ }[];
+ };
+ addons?: {
+ name: string;
+ status: string;
+ repository: string;
+ arguments?: string;
+ }[];
+ currentVersion: string;
+ kubernetesVersions: Option[];
+}
+
+async function getAddons(environmentID: number) {
+ try {
+ const { data } = await axios.get(
+ `cloud/endpoints/${environmentID}/addons`
+ );
+ return data;
+ } catch (err) {
+ throw parseAxiosError(err as Error, 'Unable to retrieve addons');
+ }
+}
+
+async function upgradeCluster(environmentID: number, nextVersion: string) {
+ try {
+ const { data } = await axios.post(
+ `cloud/endpoints/${environmentID}/upgrade`,
+ { nextVersion }
+ );
+ return data;
+ } catch (err) {
+ throw parseAxiosError(
+ err as Error,
+ 'Unable to send upgrade cluster request'
+ );
+ }
+}
+
+async function updateAddons(
+ environmentID: number,
+ payload: { addons: AddOnFormValue[] }
+) {
+ try {
+ const { data } = await axios.post(
+ `cloud/endpoints/${environmentID}/addons`,
+ payload
+ );
+ return data;
+ } catch (err) {
+ throw parseAxiosError(err as Error, 'Unable to update addons');
+ }
+}
+
+export function useAddonsQuery(
+ environmentID?: number,
+ status?: number,
+ select?: (info: AddonsResponse | null) => TSelect
+) {
+ return useQuery(
+ ['environments', environmentID, 'clusterInfo', 'addons'],
+ () => (environmentID ? getAddons(environmentID) : null),
+ {
+ select,
+ enabled: !!environmentID && status !== EnvironmentStatus.Error,
+ }
+ );
+}
+
+type UpdateAddOns = {
+ environmentID: number;
+ credentialID: number;
+ payload: { addons: AddOnFormValue[] };
+};
+
+type UpgradeRequest = {
+ environmentID: number;
+ nextVersion: string;
+};
+
+export function useUpdateAddonsMutation() {
+ return useMutation(
+ ({ environmentID, payload }: UpdateAddOns) =>
+ updateAddons(environmentID, payload),
+ withError('Failed to update addons')
+ );
+}
+
+export function useUpgradeClusterMutation() {
+ return useMutation(
+ ({ environmentID, nextVersion }: UpgradeRequest) =>
+ upgradeCluster(environmentID, nextVersion),
+ withError('Failed to send upgrade cluster request')
+ );
+}
diff --git a/app/react/kubernetes/cluster/microk8s/addons/types.ts b/app/react/kubernetes/cluster/microk8s/addons/types.ts
new file mode 100644
index 000000000..033efea88
--- /dev/null
+++ b/app/react/kubernetes/cluster/microk8s/addons/types.ts
@@ -0,0 +1,32 @@
+export interface AddOnFormValue {
+ name: string;
+ arguments?: string;
+ repository?: string;
+ disableSelect?: boolean;
+
+ info?: string;
+}
+
+export type K8sAddOnsForm = {
+ addons: AddOnFormValue[];
+ currentVersion: string;
+};
+
+export type AddonsArgumentType = 'required' | 'optional' | '';
+
+export type AddOnOption = {
+ label: string;
+ name: string;
+ repository?: string;
+
+ arguments?: string;
+ tooltip?: string;
+ placeholder?: string;
+ argumentsType?: AddonsArgumentType;
+ selectedLabel?: string;
+};
+
+export type GroupedAddonOptions = {
+ label: string;
+ options: AddOnOption[];
+}[];
diff --git a/app/react/portainer/environments/ItemView/DisassociateButton.tsx b/app/react/portainer/environments/ItemView/DisassociateButton.tsx
new file mode 100644
index 000000000..4cce6359c
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/DisassociateButton.tsx
@@ -0,0 +1,40 @@
+import { notifySuccess } from '@/portainer/services/notifications';
+
+import { LoadingButton } from '@@/buttons';
+
+import { Environment } from '../types';
+import { useDisassociateEdgeEnvironment } from '../queries/useDisassociateEdgeEnvironment';
+
+import { confirmDisassociate } from './ConfirmDisassociateModel';
+
+export function DisassociateButton({
+ environment,
+}: {
+ environment: Environment;
+}) {
+ const mutation = useDisassociateEdgeEnvironment();
+
+ return (
+
+ Disassociate
+
+ );
+
+ async function handleClick() {
+ if (!(await confirmDisassociate())) {
+ return;
+ }
+
+ mutation.mutate(environment.Id, {
+ onSuccess() {
+ notifySuccess('Environment disassociated', environment.Name);
+ },
+ });
+ }
+}
diff --git a/app/react/portainer/environments/ItemView/EdgeAssociationInfo.tsx b/app/react/portainer/environments/ItemView/EdgeAssociationInfo.tsx
new file mode 100644
index 000000000..068ae42d5
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/EdgeAssociationInfo.tsx
@@ -0,0 +1,33 @@
+import { InformationPanel } from '@@/InformationPanel';
+import { TextTip } from '@@/Tip/TextTip';
+
+import { getPlatformTypeName } from '../utils';
+import { Environment } from '../types';
+
+import { DisassociateButton } from './DisassociateButton';
+
+export function EdgeAssociationInfo({
+ environment,
+}: {
+ environment: Environment;
+}) {
+ const platform = getPlatformTypeName(environment.Type);
+ return (
+
+
+ This Edge environment is associated to an Edge environment ({platform}).
+
+
+
+
+ Edge key: {environment.EdgeKey}
+
+
+ Edge identifier: {environment.EdgeID}
+
+
+
+
+
+ );
+}
diff --git a/app/react/portainer/environments/ItemView/EdgeDeploymentInfo.tsx b/app/react/portainer/environments/ItemView/EdgeDeploymentInfo.tsx
new file mode 100644
index 000000000..5aa27dd40
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/EdgeDeploymentInfo.tsx
@@ -0,0 +1,69 @@
+import _ from 'lodash';
+
+import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
+import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
+
+import { Widget } from '@@/Widget';
+import { FormSection } from '@@/form-components/FormSection';
+import { TextTip } from '@@/Tip/TextTip';
+
+import { Environment } from '../types';
+
+import { EdgeKeyDisplay } from './EdgeKeyDisplay';
+
+export function EdgeDeploymentInfo({
+ environment,
+}: {
+ environment: Environment;
+}) {
+ const edgeKeyDetails = decodeEdgeKey(environment.EdgeKey);
+
+ return (
+
+
+
+
+
+ Refer to the platform related command below to deploy the Edge
+ agent in your remote cluster.
+
+
+ The agent will communicate with Portainer via{' '}
+ {edgeKeyDetails.instanceURL} and{' '}
+ tcp://{edgeKeyDetails.tunnelServerAddr}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function decodeEdgeKey(key: string) {
+ if (key === '') {
+ return {};
+ }
+
+ const decodedKey = atob(key).split('|');
+ return {
+ instanceURL: decodedKey[0],
+ tunnelServerAddr: decodedKey[1],
+ };
+}
diff --git a/app/react/portainer/environments/ItemView/EdgeEnvironmentDetails.tsx b/app/react/portainer/environments/ItemView/EdgeEnvironmentDetails.tsx
new file mode 100644
index 000000000..a14d3943f
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/EdgeEnvironmentDetails.tsx
@@ -0,0 +1,22 @@
+import { Environment } from '../types';
+
+import { EdgeDeploymentInfo } from './EdgeDeploymentInfo';
+import { EdgeAssociationInfo } from './EdgeAssociationInfo';
+
+export function EdgeEnvironmentDetails({
+ environment,
+}: {
+ environment: Environment;
+}) {
+ return (
+
+
+ {environment.EdgeID ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/app/react/portainer/environments/ItemView/ItemView.tsx b/app/react/portainer/environments/ItemView/ItemView.tsx
new file mode 100644
index 000000000..dd0675803
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/ItemView.tsx
@@ -0,0 +1,45 @@
+import { useCurrentStateAndParams } from '@uirouter/react';
+
+import { PageHeader } from '@@/PageHeader';
+
+import { useEnvironment } from '../queries';
+import { isEdgeEnvironment } from '../utils';
+
+import { UpdateForm } from './UpdateForm/UpdateForm';
+import { EdgeEnvironmentDetails } from './EdgeEnvironmentDetails';
+import { KubeDetails } from './KubeDetails';
+
+export function ItemView() {
+ const {
+ params: { id },
+ } = useCurrentStateAndParams();
+ const environmentQuery = useEnvironment(id);
+
+ if (!environmentQuery.data) {
+ return null;
+ }
+
+ const environment = environmentQuery.data;
+ const isEdge = isEdgeEnvironment(environment.Type);
+
+ return (
+ <>
+
+
+ {isEdge && }
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/app/react/portainer/environments/ItemView/KaaSClusterDetails.tsx b/app/react/portainer/environments/ItemView/KaaSClusterDetails.tsx
new file mode 100644
index 000000000..c44a6a5ac
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/KaaSClusterDetails.tsx
@@ -0,0 +1,71 @@
+import KubeIcon from '@/assets/ico/kube.svg?c';
+
+import { Widget } from '@@/Widget';
+import { Button } from '@@/buttons';
+
+import { CloudProviderSettings } from '../types';
+
+export function KaaSClusterDetails({ info }: { info: CloudProviderSettings }) {
+ return (
+
+
+
+
+
+
+
+
+
+ | Provider |
+
+ {info.Name}
+
+
+
+
+ |
+
+ {!!info.Region && (
+
+ | Region |
+ {info.Region} |
+
+ )}
+ {!!info.Size && (
+
+ | Node Size |
+ {info.Size} |
+
+ )}
+ {!!info.NetworkID && (
+
+ | Network Id |
+ {info.NetworkID} |
+
+ )}
+ {!!info.NodeIPs && (
+
+ | Node IPs |
+ {info.NodeIPs} |
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/app/react/portainer/environments/ItemView/KubeConfigureInstructions.tsx b/app/react/portainer/environments/ItemView/KubeConfigureInstructions.tsx
new file mode 100644
index 000000000..db4cf7e7b
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/KubeConfigureInstructions.tsx
@@ -0,0 +1,30 @@
+import { Wrench } from 'lucide-react';
+
+import { InformationPanel } from '@@/InformationPanel';
+import { TextTip } from '@@/Tip/TextTip';
+import { Link } from '@@/Link';
+
+import { EnvironmentId } from '../types';
+
+export function KubeConfigureInstructions({
+ environmentId,
+}: {
+ environmentId: EnvironmentId;
+}) {
+ return (
+
+
+ You should configure the features available in this Kubernetes
+ environment in the{' '}
+
+ Kubernetes configuration
+ {' '}
+ view.
+
+
+ );
+}
diff --git a/app/react/portainer/environments/ItemView/KubeDetails.tsx b/app/react/portainer/environments/ItemView/KubeDetails.tsx
new file mode 100644
index 000000000..6987fe3e2
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/KubeDetails.tsx
@@ -0,0 +1,31 @@
+import { isEdgeEnvironment, isKubernetesEnvironment } from '../utils';
+import { Environment, EnvironmentStatus } from '../types';
+import { k8sInstallTitles } from '../wizard/EnvironmentsCreationView/WizardK8sInstall/types';
+
+import { KubeConfigureInstructions } from './KubeConfigureInstructions';
+import { Microk8sClusterDetails } from './Microk8sClusterDetails';
+import { KaaSClusterDetails } from './KaaSClusterDetails';
+
+export function KubeDetails({ environment }: { environment: Environment }) {
+ const isKube = isKubernetesEnvironment(environment.Type);
+ const isEdge = isEdgeEnvironment(environment.Type);
+
+ return (
+ <>
+ {isKube &&
+ (!isEdge || !!environment.EdgeID) &&
+ environment.Status !== EnvironmentStatus.Error && (
+
+ )}
+
+ {environment.CloudProvider?.Name.toLowerCase() ===
+ k8sInstallTitles.microk8s.toLowerCase() ? (
+
+ ) : (
+ environment.CloudProvider?.URL && (
+
+ )
+ )}
+ >
+ );
+}
diff --git a/app/react/portainer/environments/ItemView/Microk8sClusterDetails.tsx b/app/react/portainer/environments/ItemView/Microk8sClusterDetails.tsx
new file mode 100644
index 000000000..882563eb6
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/Microk8sClusterDetails.tsx
@@ -0,0 +1,74 @@
+import Kube from '@/assets/ico/kube.svg?c';
+import { useEnvironment } from '@/react/portainer/environments/queries';
+import { useAddonsQuery } from '@/react/kubernetes/cluster/microk8s/addons/addons.service';
+import { useNodesQuery } from '@/react/kubernetes/cluster/HomeView/nodes.service';
+
+import { Widget, WidgetTitle, WidgetBody } from '@@/Widget';
+import { DetailsTable } from '@@/DetailsTable';
+import { TextTip } from '@@/Tip/TextTip';
+import { Link } from '@@/Link';
+
+import { EnvironmentId } from '../types';
+
+export function Microk8sClusterDetails({
+ environmentId,
+}: {
+ environmentId: EnvironmentId;
+}) {
+ const { data: environment, ...environmentQuery } =
+ useEnvironment(environmentId);
+ const { data: addonResponse, ...addonsQuery } = useAddonsQuery(
+ environmentId,
+ environment?.Status
+ );
+
+ const { data: nodes, ...nodesQuery } = useNodesQuery(environmentId);
+ const currentVersion = addonResponse?.currentVersion;
+ const addonNames = addonResponse?.addons
+ ?.filter((addon) => addon.status === 'enabled')
+ .map((addon) => addon.name);
+
+ if (environmentQuery.isError) {
+ return Unable to load environment;
+ }
+
+ return (
+
+
+
+
+
+
+
+ {addonsQuery.isError && 'Unable to get addons'}
+ {!addonNames?.length &&
+ addonsQuery.isSuccess &&
+ 'No addons installed'}
+ {addonNames?.length && addonNames.join(', ')}
+
+
+ {addonsQuery.isError && 'Unable to find kubernetes version'}
+ {!!currentVersion && currentVersion}
+
+
+ {nodesQuery.isError && 'Unable to get node count'}
+ {nodes && nodes.length}
+
+
+
+ You can{' '}
+
+ manage the cluster
+ {' '}
+ to upgrade, add/remove nodes or enable/disable addons.
+
+
+
+
+
+ );
+}
diff --git a/app/react/portainer/environments/ItemView/UpdateForm/AMTInfo.tsx b/app/react/portainer/environments/ItemView/UpdateForm/AMTInfo.tsx
new file mode 100644
index 000000000..deb519652
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/UpdateForm/AMTInfo.tsx
@@ -0,0 +1,89 @@
+import { useAMTInfo } from '@/react/edge/edge-devices/open-amt/useAmtInfo';
+
+import { FormControl } from '@@/form-components/FormControl';
+import { FormSection } from '@@/form-components/FormSection';
+import { Input } from '@@/form-components/Input';
+
+import { EnvironmentId } from '../../types';
+import { useSettings } from '../../../settings/queries';
+
+export function AmtInfo({ environmentId }: { environmentId: EnvironmentId }) {
+ const isAmtEnabledQuery = useSettings(
+ (settings) => settings.openAMTConfiguration.enabled
+ );
+ const amtQuery = useAMTInfo(environmentId, {
+ enabled: isAmtEnabledQuery.data,
+ });
+
+ if (!isAmtEnabledQuery.data) {
+ return null;
+ }
+
+ const info = amtQuery.data;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/react/portainer/environments/ItemView/UpdateForm/AgentEnvironmentAddress.tsx b/app/react/portainer/environments/ItemView/UpdateForm/AgentEnvironmentAddress.tsx
new file mode 100644
index 000000000..30ad0bf79
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/UpdateForm/AgentEnvironmentAddress.tsx
@@ -0,0 +1,26 @@
+import { useField } from 'formik';
+
+import { FormControl } from '@@/form-components/FormControl';
+import { Input } from '@@/form-components/Input';
+
+export function AgentAddressField() {
+ const [{ value, onChange }, { error }] = useField('url');
+
+ return (
+
+
+
+ );
+}
diff --git a/app/react/portainer/environments/ItemView/UpdateForm/AzureConfiguration.tsx b/app/react/portainer/environments/ItemView/UpdateForm/AzureConfiguration.tsx
new file mode 100644
index 000000000..abf11953d
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/UpdateForm/AzureConfiguration.tsx
@@ -0,0 +1,65 @@
+import { FormControl } from '@@/form-components/FormControl';
+import { FormSection } from '@@/form-components/FormSection';
+import { Input } from '@@/form-components/Input';
+import { FieldsetValues, SetFieldValue } from '@@/form-components/formikUtils';
+
+export interface AzureFormValues {
+ applicationId: string;
+ tenantId: string;
+ authKey: string;
+}
+
+export function AzureEnvironmentConfiguration({
+ values,
+ errors,
+ setFieldValue,
+}: FieldsetValues & {
+ setFieldValue: SetFieldValue;
+}) {
+ return (
+
+
+ setFieldValue('applicationId', e.target.value)}
+ data-cy="azure-credential-appid-input"
+ />
+
+
+ setFieldValue('tenantId', e.target.value)}
+ data-cy="azure-credential-tenantid-input"
+ />
+
+
+ setFieldValue('authKey', e.target.value)}
+ data-cy="azure-credential-authkey-input"
+ />
+
+
+ );
+}
diff --git a/app/react/portainer/environments/ItemView/UpdateForm/PublicIPField.tsx b/app/react/portainer/environments/ItemView/UpdateForm/PublicIPField.tsx
new file mode 100644
index 000000000..1367a0dee
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/UpdateForm/PublicIPField.tsx
@@ -0,0 +1,25 @@
+import { useField } from 'formik';
+
+import { FormControl } from '@@/form-components/FormControl';
+import { Input } from '@@/form-components/Input';
+
+export function PublicIPField() {
+ const [{ value, onChange }, { error }] = useField('publicUrl');
+ return (
+
+
+
+ );
+}
diff --git a/app/react/portainer/environments/ItemView/UpdateForm/URLField.tsx b/app/react/portainer/environments/ItemView/UpdateForm/URLField.tsx
new file mode 100644
index 000000000..b24c2f883
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/UpdateForm/URLField.tsx
@@ -0,0 +1,27 @@
+import { useField } from 'formik';
+
+import { FormControl } from '@@/form-components/FormControl';
+import { Input } from '@@/form-components/Input';
+
+export function URLField({ disabled }: { disabled?: boolean }) {
+ const [{ value, onChange }, { error }] = useField('url');
+
+ return (
+
+
+
+ );
+}
diff --git a/app/react/portainer/environments/ItemView/UpdateForm/UpdateForm.tsx b/app/react/portainer/environments/ItemView/UpdateForm/UpdateForm.tsx
new file mode 100644
index 000000000..17c6881b3
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/UpdateForm/UpdateForm.tsx
@@ -0,0 +1,177 @@
+import { Form, Formik } from 'formik';
+
+import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField';
+import { EdgeAsyncIntervalsForm } from '@/react/edge/components/EdgeAsyncIntervalsForm';
+import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
+
+import { Widget } from '@@/Widget';
+import { FormSection } from '@@/form-components/FormSection';
+import { TLSFieldset } from '@@/TLSFieldset';
+import { FormActions } from '@@/form-components/FormActions';
+import { Button } from '@@/buttons';
+import { Link } from '@@/Link';
+import { TextTip } from '@@/Tip/TextTip';
+
+import { Environment, EnvironmentStatus, EnvironmentType } from '../../types';
+import { NameField } from '../../common/NameField';
+import {
+ isAgentEnvironment,
+ isDockerAPIEnvironment,
+ isDockerEnvironment,
+ isEdgeEnvironment,
+ isLocalEnvironment,
+} from '../../utils';
+import { MetadataFieldset } from '../../common/MetadataFieldset';
+
+import { AzureEnvironmentConfiguration } from './AzureConfiguration';
+import { URLField } from './URLField';
+import { PublicIPField } from './PublicIPField';
+import { FormValues } from './types';
+import { useUpdateMutation } from './useUpdateMutation';
+import { AgentAddressField } from './AgentEnvironmentAddress';
+import { AmtInfo } from './AMTInfo';
+
+export function UpdateForm({ environment }: { environment: Environment }) {
+ const isEdge = isEdgeEnvironment(environment.Type);
+ const isAgent = isAgentEnvironment(environment.Type);
+ const isLocal = isLocalEnvironment(environment);
+ const isAzure = environment.Type === EnvironmentType.Azure;
+ const { isLoading, handleSubmit } = useUpdateMutation(environment, {
+ isEdge,
+ isLocal,
+ isAzure,
+ });
+
+ const isAmtVisible =
+ isDockerEnvironment(environment.Type) && isEdge && !!environment.EdgeID;
+ const isErrorState = environment.Status === EnvironmentStatus.Error;
+ const initialValues: FormValues = {
+ name: environment.Name,
+ url: environment.URL,
+ publicUrl: environment.PublicURL || '',
+
+ tlsConfig: {
+ tls: environment.TLSConfig.TLS || false,
+ skipVerify: environment.TLSConfig.TLSSkipVerify || false,
+ },
+ meta: {
+ tagIds: environment.TagIds,
+ groupId: environment.GroupId,
+ },
+ azure: {
+ applicationId: environment.AzureCredentials?.ApplicationID || '',
+ tenantId: environment.AzureCredentials?.TenantID || '',
+ authKey: environment.AzureCredentials?.AuthenticationKey || '',
+ },
+ edge: {
+ checkInInterval: environment.EdgeCheckinInterval || 0,
+ CommandInterval: environment.Edge.CommandInterval || 0,
+ PingInterval: environment.Edge.PingInterval || 0,
+ SnapshotInterval: environment.Edge.SnapshotInterval || 0,
+ },
+ };
+
+ return (
+
+
+
+
+
+ {({ values, errors, setFieldValue, setValues, isValid }) => (
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/app/react/portainer/environments/ItemView/UpdateForm/types.ts b/app/react/portainer/environments/ItemView/UpdateForm/types.ts
new file mode 100644
index 000000000..dad64eedd
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/UpdateForm/types.ts
@@ -0,0 +1,23 @@
+import { TagId } from '@/portainer/tags/types';
+import { EdgeAsyncIntervalsValues } from '@/react/edge/components/EdgeAsyncIntervalsForm';
+
+import { TLSConfig } from '@@/TLSFieldset/types';
+
+import { EnvironmentGroupId } from '../../environment-groups/types';
+
+import { AzureFormValues } from './AzureConfiguration';
+
+export interface FormValues {
+ name: string;
+ url: string;
+ publicUrl: string;
+ tlsConfig: TLSConfig;
+ azure: AzureFormValues;
+ meta: {
+ tagIds: TagId[];
+ groupId: EnvironmentGroupId;
+ };
+ edge: {
+ checkInInterval: number;
+ } & EdgeAsyncIntervalsValues;
+}
diff --git a/app/react/portainer/environments/ItemView/UpdateForm/useUpdateMutation.tsx b/app/react/portainer/environments/ItemView/UpdateForm/useUpdateMutation.tsx
new file mode 100644
index 000000000..5ba4e0da9
--- /dev/null
+++ b/app/react/portainer/environments/ItemView/UpdateForm/useUpdateMutation.tsx
@@ -0,0 +1,106 @@
+import _ from 'lodash';
+import { useCurrentStateAndParams, useRouter } from '@uirouter/react';
+
+import { notifySuccess } from '@/portainer/services/notifications';
+
+import { confirmDestructive } from '@@/modals/confirm';
+import { buildConfirmButton } from '@@/modals/utils';
+
+import { Environment, EnvironmentType } from '../../types';
+import {
+ UpdateEnvironmentPayload,
+ useUpdateEnvironmentMutation,
+} from '../../queries/useUpdateEnvironmentMutation';
+import { isDockerEnvironment, isKubernetesEnvironment } from '../../utils';
+
+import { FormValues } from './types';
+
+export function useUpdateMutation(
+ environment: Environment,
+ {
+ isEdge,
+ isLocal,
+ isAzure,
+ }: {
+ isEdge: boolean;
+ isLocal: boolean;
+ isAzure: boolean;
+ }
+) {
+ const updateMutation = useUpdateEnvironmentMutation();
+ const router = useRouter();
+ const { params: stateParams } = useCurrentStateAndParams();
+
+ return {
+ handleSubmit,
+ isLoading: updateMutation.isLoading,
+ };
+
+ async function handleSubmit(values: FormValues) {
+ if (
+ isEdge &&
+ _.difference(environment.TagIds, values.meta.tagIds).length > 0
+ ) {
+ const confirmed = await confirmDestructive({
+ title: 'Confirm action',
+ message:
+ 'Removing tags from this environment will remove the corresponding edge stacks when dynamic grouping is being used',
+ confirmButton: buildConfirmButton(),
+ });
+
+ if (!confirmed) {
+ return;
+ }
+ }
+
+ const payload: UpdateEnvironmentPayload = {
+ Name: values.name,
+ PublicURL: values.publicUrl,
+ GroupID: values.meta.groupId,
+ TagIDs: values.meta.tagIds,
+ AzureApplicationID: values.azure.applicationId,
+ AzureTenantID: values.azure.tenantId,
+ AzureAuthenticationKey: values.azure.authKey,
+ EdgeCheckinInterval: values.edge.checkInInterval,
+ Edge: {
+ CommandInterval: values.edge.CommandInterval,
+ PingInterval: values.edge.PingInterval,
+ SnapshotInterval: values.edge.SnapshotInterval,
+ },
+ };
+
+ if (isLocal && !isAzure && !isKubernetesEnvironment(environment.Type)) {
+ payload.URL = `tcp://${values.url}`;
+
+ if (isDockerEnvironment(environment.Type)) {
+ const { tlsConfig } = values;
+ payload.TLS = tlsConfig.tls;
+ payload.TLSSkipVerify = tlsConfig.skipVerify || false;
+ if (tlsConfig.tls && !tlsConfig.skipVerify) {
+ // payload.TLSSkipClientVerify = tlsConfig.skipClientVerify;
+ payload.TLSCACert = tlsConfig.caCertFile;
+ payload.TLSCert = tlsConfig.certFile;
+ payload.TLSKey = tlsConfig.keyFile;
+ }
+ }
+ }
+
+ if (environment.Type === EnvironmentType.AgentOnKubernetes) {
+ payload.URL = values.url;
+ }
+
+ if (environment.Type === EnvironmentType.KubernetesLocal) {
+ payload.URL = `https://${values.url}`;
+ }
+
+ updateMutation.mutate(
+ { id: environment.Id, payload },
+ {
+ onSuccess() {
+ notifySuccess('Environment updated', environment.Name);
+ router.stateService.go(stateParams.redirectTo || '^');
+ },
+ }
+ );
+ }
+}
diff --git a/app/react/portainer/environments/ItemView/tls-options.tsx b/app/react/portainer/environments/ItemView/tls-options.tsx
deleted file mode 100644
index 5b6ea16e4..000000000
--- a/app/react/portainer/environments/ItemView/tls-options.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Shield } from 'lucide-react';
-
-import { BoxSelectorOption } from '@@/BoxSelector';
-
-export const tlsOptions: ReadonlyArray> = [
- {
- id: 'tls_client_ca',
- value: 'tls_client_ca',
- icon: Shield,
- iconType: 'badge',
- label: 'TLS with server and client verification',
- description: 'Use client certificates and server verification',
- },
- {
- id: 'tls_client_noca',
- value: 'tls_client_noca',
- icon: Shield,
- iconType: 'badge',
- label: 'TLS with client verification only',
- description: 'Use client certificates without server verification',
- },
- {
- id: 'tls_ca',
- value: 'tls_ca',
- icon: Shield,
- iconType: 'badge',
- label: 'TLS with server verification only',
- description: 'Only verify the server certificate',
- },
- {
- id: 'tls_only',
- value: 'tls_only',
- icon: Shield,
- iconType: 'badge',
- label: 'TLS only',
- description: 'No server/client verification',
- },
-] as const;
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/GroupsField.tsx b/app/react/portainer/environments/common/MetadataFieldset/GroupsField.tsx
similarity index 100%
rename from app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/GroupsField.tsx
rename to app/react/portainer/environments/common/MetadataFieldset/GroupsField.tsx
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/MetadataFieldset.tsx b/app/react/portainer/environments/common/MetadataFieldset/MetadataFieldset.tsx
similarity index 100%
rename from app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/MetadataFieldset.tsx
rename to app/react/portainer/environments/common/MetadataFieldset/MetadataFieldset.tsx
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/index.ts b/app/react/portainer/environments/common/MetadataFieldset/index.ts
similarity index 100%
rename from app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/index.ts
rename to app/react/portainer/environments/common/MetadataFieldset/index.ts
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/validation.ts b/app/react/portainer/environments/common/MetadataFieldset/validation.ts
similarity index 100%
rename from app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/validation.ts
rename to app/react/portainer/environments/common/MetadataFieldset/validation.ts
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/NameField.tsx b/app/react/portainer/environments/common/NameField.tsx
similarity index 95%
rename from app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/NameField.tsx
rename to app/react/portainer/environments/common/NameField.tsx
index 527becfe1..7286b19af 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/NameField.tsx
+++ b/app/react/portainer/environments/common/NameField.tsx
@@ -12,12 +12,14 @@ interface Props {
readonly?: boolean;
tooltip?: string;
placeholder?: string;
+ disabled?: boolean;
}
export function NameField({
readonly,
tooltip,
placeholder = 'e.g. docker-prod01 / kubernetes-cluster01',
+ disabled,
}: Props) {
const [{ value }, meta, { setValue }] = useField('name');
@@ -35,12 +37,13 @@ export function NameField({
>
setDebouncedValue(e.target.value)}
value={debouncedValue}
placeholder={placeholder}
readOnly={readonly}
+ disabled={disabled}
/>
);
diff --git a/app/react/portainer/environments/environment.service/index.ts b/app/react/portainer/environments/environment.service/index.ts
index 35b7f623c..4df64215a 100644
--- a/app/react/portainer/environments/environment.service/index.ts
+++ b/app/react/portainer/environments/environment.service/index.ts
@@ -167,14 +167,6 @@ export async function endpointsByGroup(
});
}
-export async function disassociateEndpoint(id: EnvironmentId) {
- try {
- await axios.delete(buildUrl(id, 'association'));
- } catch (e) {
- throw parseAxiosError(e as Error);
- }
-}
-
export async function deleteEndpoint(id: EnvironmentId) {
try {
await axios.delete(buildUrl(id));
diff --git a/app/react/portainer/environments/queries/useDisassociateEdgeEnvironment.ts b/app/react/portainer/environments/queries/useDisassociateEdgeEnvironment.ts
new file mode 100644
index 000000000..9dae14454
--- /dev/null
+++ b/app/react/portainer/environments/queries/useDisassociateEdgeEnvironment.ts
@@ -0,0 +1,26 @@
+import { useMutation } from 'react-query';
+
+import { useAnalytics } from '@/react/hooks/useAnalytics';
+import axios, { parseAxiosError } from '@/portainer/services/axios';
+
+import { EnvironmentId } from '../types';
+import { buildUrl } from '../environment.service/utils';
+
+export function useDisassociateEdgeEnvironment() {
+ const { trackEvent } = useAnalytics();
+ return useMutation({
+ mutationFn: (environmentId: EnvironmentId) => {
+ trackEvent('edge-endpoint-disassociate', { category: 'edge' });
+
+ return disassociateEnvironment(environmentId);
+ },
+ });
+}
+
+export async function disassociateEnvironment(id: EnvironmentId) {
+ try {
+ await axios.delete(buildUrl(id, 'association'));
+ } catch (e) {
+ throw parseAxiosError(e, 'Unable to disassociate environment');
+ }
+}
diff --git a/app/react/portainer/environments/queries/useUpdateEnvironmentMutation.ts b/app/react/portainer/environments/queries/useUpdateEnvironmentMutation.ts
index 4b58ccc4a..55a6eaec5 100644
--- a/app/react/portainer/environments/queries/useUpdateEnvironmentMutation.ts
+++ b/app/react/portainer/environments/queries/useUpdateEnvironmentMutation.ts
@@ -6,14 +6,17 @@ import {
EnvironmentStatusMessage,
Environment,
KubernetesSettings,
- DeploymentOptions,
EndpointChangeWindow,
+ DeploymentOptions,
} from '@/react/portainer/environments/types';
import axios, { parseAxiosError } from '@/portainer/services/axios';
-import { TagId } from '@/portainer/tags/types';
-import { EnvironmentGroupId } from '../environment-groups/types';
import { buildUrl } from '../environment.service/utils';
+import { Pair } from '../../settings/types';
+import {
+ TeamAccessPolicies,
+ UserAccessPolicies,
+} from '../../registries/types/registry';
import { environmentQueryKeys } from './query-keys';
@@ -25,30 +28,117 @@ export function useUpdateEnvironmentMutation() {
});
}
-export interface UpdateEnvironmentPayload extends Partial {
+interface TLSFiles {
TLSCACert?: File;
TLSCert?: File;
TLSKey?: File;
+}
- Name: string;
- PublicURL: string;
- GroupID: EnvironmentGroupId;
- TagIds: TagId[];
+export interface UpdateEnvironmentPayload extends TLSFiles {
+ /**
+ * Name that will be used to identify this environment(endpoint)
+ */
+ Name?: string;
- EdgeCheckinInterval: number;
+ /**
+ * URL or IP address of a Docker host
+ */
+ URL?: string;
- TLS: boolean;
- TLSSkipVerify: boolean;
- TLSSkipClientVerify: boolean;
- AzureApplicationID: string;
- AzureTenantID: string;
- AzureAuthenticationKey: string;
+ /**
+ * URL or IP address where exposed containers will be reachable. Defaults to URL if not specified
+ */
+ PublicURL?: string;
- IsSetStatusMessage: boolean;
- StatusMessage: EnvironmentStatusMessage;
+ /**
+ * GPUs information
+ */
+ Gpus?: Pair[];
+
+ /**
+ * Group identifier
+ */
+ GroupID?: number;
+
+ /**
+ * Require TLS to connect against this environment(endpoint)
+ */
+ TLS?: boolean;
+
+ /**
+ * Skip server verification when using TLS
+ */
+ TLSSkipVerify?: boolean;
+
+ /**
+ * Skip client verification when using TLS
+ */
+ TLSSkipClientVerify?: boolean;
+
+ /**
+ * The status of the environment(endpoint) (1 - up, 2 - down)
+ */
+ Status?: number;
+
+ /**
+ * Azure application ID
+ */
+ AzureApplicationID?: string;
+
+ /**
+ * Azure tenant ID
+ */
+ AzureTenantID?: string;
+
+ /**
+ * Azure authentication key
+ */
+ AzureAuthenticationKey?: string;
+
+ /**
+ * List of tag identifiers to which this environment(endpoint) is associated
+ */
+ TagIDs?: number[];
+
+ /**
+ * User access policies for the environment
+ */
+ UserAccessPolicies?: UserAccessPolicies;
+
+ /**
+ * Team access policies for the environment
+ */
+ TeamAccessPolicies?: TeamAccessPolicies;
+
+ /**
+ * Associated Kubernetes data
+ */
Kubernetes?: KubernetesSettings;
- DeploymentOptions?: DeploymentOptions | null;
+
+ /**
+ * Whether GitOps update time restrictions are enabled
+ */
ChangeWindow?: EndpointChangeWindow;
+
+ /**
+ * Hide manual deployment forms for an environment
+ */
+ DeploymentOptions?: DeploymentOptions;
+
+ /**
+ * The check-in interval for edge agent (in seconds)
+ */
+ EdgeCheckinInterval?: number;
+
+ Edge: {
+ PingInterval?: number;
+ SnapshotInterval?: number;
+ CommandInterval?: number;
+ };
+
+ IsSetStatusMessage?: boolean;
+
+ StatusMessage?: EnvironmentStatusMessage;
}
export async function updateEnvironment({
diff --git a/app/react/portainer/environments/types.ts b/app/react/portainer/environments/types.ts
index 47b5a4284..ccc961f2f 100644
--- a/app/react/portainer/environments/types.ts
+++ b/app/react/portainer/environments/types.ts
@@ -2,6 +2,12 @@ import { TagId } from '@/portainer/tags/types';
import { EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
import { DockerSnapshot } from '@/react/docker/snapshots/types';
+import { Pair, TLSConfiguration } from '../settings/types';
+import {
+ TeamAccessPolicies,
+ UserAccessPolicies,
+} from '../registries/types/registry';
+
export type EnvironmentId = number;
export enum EnvironmentType {
@@ -107,6 +113,54 @@ export type DeploymentOptions = {
hideFileUpload: boolean;
};
+type AddonWithArgs = {
+ Name: string;
+ Args?: string;
+};
+
+export enum K8sDistributionType {
+ MICROK8S = 'microk8s',
+}
+
+export enum KaasProvider {
+ CIVO = 'civo',
+ LINODE = 'linode',
+ DIGITAL_OCEAN = 'digitalocean',
+ GOOGLE_CLOUD = 'gke',
+ AWS = 'amazon',
+ AZURE = 'azure',
+}
+
+export type CloudProviderSettings = {
+ Name:
+ | 'Civo'
+ | 'Linode'
+ | 'Digital Ocean'
+ | 'Google'
+ | 'Azure'
+ | 'Amazon'
+ | 'MicroK8s';
+ Provider: K8sDistributionType | KaasProvider;
+ URL: string;
+ Region: string | null;
+ Size: number | null;
+ NodeCount: number;
+ CPU: number | null;
+ AddonsWithArgs: AddonWithArgs[] | null;
+ AmiType: number | null;
+ CredentialID: number;
+ DNSPrefix: string;
+ HDD: number | null;
+ InstanceType: string | null;
+ KubernetesVersion: string;
+ NetworkID: number | null;
+ NodeIPs: string;
+ NodeVolumeSize: number | null;
+ PoolName: string;
+ RAM: number | null;
+ ResourceGroup: string;
+ Tier: string;
+};
/**
* EndpointChangeWindow determine when GitOps stack/app updates may occur
*/
@@ -120,42 +174,187 @@ export interface EnvironmentStatusMessage {
detail: string;
}
-export type Environment = {
- Agent: { Version: string };
- Id: EnvironmentId;
- Type: EnvironmentType;
- TagIds: TagId[];
- GroupId: EnvironmentGroupId;
- DeploymentOptions: DeploymentOptions | null;
- EnableGPUManagement: boolean;
- EdgeID?: string;
- EdgeKey: string;
- EdgeCheckinInterval?: number;
- QueryDate?: number;
- Heartbeat?: boolean;
- LastCheckInDate?: number;
- Name: string;
- Status: EnvironmentStatus;
- URL: string;
- Snapshots: DockerSnapshot[];
- Kubernetes: KubernetesSettings;
- PublicURL?: string;
- UserTrusted: boolean;
- AMTDeviceGUID?: string;
- Edge: EnvironmentEdge;
- SecuritySettings: EnvironmentSecuritySettings;
- Gpus?: { name: string; value: string }[];
- EnableImageNotification: boolean;
- LocalTimeZone?: string;
-
- /** GitOps update change window restriction for stacks and apps */
- ChangeWindow: EndpointChangeWindow;
- /**
- * A message that describes the status. Should be included for Status Provisioning or Error.
- */
- StatusMessage?: EnvironmentStatusMessage;
+type AzureCredentials = {
+ ApplicationID: string;
+ TenantID: string;
+ AuthenticationKey: string;
};
+/**
+ * Represents an environment with all the info required to connect to it.
+ */
+export interface Environment {
+ /**
+ * Environment Identifier
+ */
+ Id: number;
+
+ /**
+ * Environment name
+ */
+ Name: string;
+
+ /**
+ * Environment type
+ */
+ Type: EnvironmentType;
+
+ /**
+ * URL or IP address of the Docker host associated with this environment.
+ */
+ URL: string;
+
+ /**
+ * Environment group identifier
+ */
+ GroupId: EnvironmentGroupId;
+
+ /**
+ * URL or IP address where exposed containers will be reachable
+ */
+ PublicURL: string;
+
+ /**
+ * List of GPU configurations associated with this environment.
+ */
+ Gpus: Pair[];
+
+ /**
+ * TLS configuration for connecting to the Docker host.
+ */
+ TLSConfig: TLSConfiguration;
+
+ /**
+ * Azure credentials if the environment is an Azure environment.
+ */
+ AzureCredentials?: AzureCredentials;
+
+ /**
+ * List of tag identifiers associated with this environment.
+ */
+ TagIds: TagId[];
+
+ /**
+ * The status of the environment (1 - up, 2 - down, 3 - provisioning, 4 - error).
+ */
+ Status: EnvironmentStatus;
+
+ /**
+ * A message that describes the status. Should be included for Status 3 or 4.
+ */
+ StatusMessage: EnvironmentStatusMessage;
+
+ /**
+ * Cloud provider information if the environment was created using KaaS provisioning.
+ */
+ CloudProvider?: CloudProviderSettings;
+
+ /**
+ * List of snapshots associated with this environment.
+ */
+ Snapshots: DockerSnapshot[];
+
+ /**
+ * User access policies for connecting to this environment.
+ */
+ UserAccessPolicies: UserAccessPolicies;
+
+ /**
+ * Team access policies for connecting to this environment.
+ */
+ TeamAccessPolicies: TeamAccessPolicies;
+
+ /**
+ * The identifier of the edge agent associated with this environment.
+ */
+ EdgeID?: string;
+
+ /**
+ * The key used to map the agent to Portainer.
+ */
+ EdgeKey: string;
+
+ /**
+ * Associated Kubernetes data.
+ */
+ Kubernetes: KubernetesSettings;
+
+ /**
+ * Maximum version of docker-compose.
+ */
+ ComposeSyntaxMaxVersion: string;
+
+ /**
+ * Environment-specific security settings.
+ */
+ SecuritySettings: EnvironmentSecuritySettings;
+
+ /**
+ * The identifier of the AMT Device associated with this environment.
+ */
+ AMTDeviceGUID?: string;
+
+ /**
+ * Mark last check-in date on check-in.
+ */
+ LastCheckInDate: number;
+
+ /**
+ * Query date of each query with the endpoints list.
+ */
+ QueryDate: number;
+
+ /**
+ * Heartbeat status of an edge environment.
+ */
+ Heartbeat: boolean;
+
+ /**
+ * Whether the device has been trusted by the user.
+ */
+ UserTrusted: boolean;
+
+ /**
+ * The check-in interval for the edge agent (in seconds).
+ */
+ EdgeCheckinInterval: number;
+
+ /**
+ * Edge settings for the environment.
+ */
+ Edge: EnvironmentEdge;
+
+ /**
+ * Agent data for the environment.
+ */
+ Agent: { Version?: string; PreviousVersion?: string };
+
+ /**
+ * Local time zone of the endpoint.
+ */
+ LocalTimeZone: string;
+
+ /**
+ * Change window restriction for GitOps updates.
+ */
+ ChangeWindow: EndpointChangeWindow;
+
+ /**
+ * Deployment options for the environment.
+ */
+ DeploymentOptions?: DeploymentOptions;
+
+ /**
+ * Enable image notification for the environment.
+ */
+ EnableImageNotification: boolean;
+
+ /**
+ * Enable GPU management for the environment.
+ */
+ EnableGPUManagement: boolean;
+}
+
/**
* TS reference of endpoint_create.go#EndpointCreationType iota
*/
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardAzure/WizardAzure.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardAzure/WizardAzure.tsx
index 4df2b6fc4..5bfd01946 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardAzure/WizardAzure.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardAzure/WizardAzure.tsx
@@ -14,9 +14,9 @@ import { FormControl } from '@@/form-components/FormControl';
import { BoxSelector, BoxSelectorOption } from '@@/BoxSelector';
import { BadgeIcon } from '@@/BadgeIcon';
-import { NameField, useNameValidation } from '../shared/NameField';
+import { NameField, useNameValidation } from '../../../common/NameField';
import { AnalyticsStateKey } from '../types';
-import { metadataValidation } from '../shared/MetadataFieldset/validation';
+import { metadataValidation } from '../../../common/MetadataFieldset/validation';
import { MoreSettingsSection } from '../shared/MoreSettingsSection';
interface FormValues {
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx
index a88fc33de..f82b690af 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx
@@ -14,7 +14,7 @@ import { LoadingButton } from '@@/buttons/LoadingButton';
import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
-import { NameField } from '../../shared/NameField';
+import { NameField } from '../../../../common/NameField';
import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
import { useValidation } from './APIForm.validation';
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.validation.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.validation.tsx
index 786129eb3..b8a31dd9e 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.validation.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.validation.tsx
@@ -2,8 +2,8 @@ import { object, SchemaOf, string } from 'yup';
import { tlsConfigValidation } from '@/react/components/TLSFieldset/TLSFieldset';
-import { metadataValidation } from '../../shared/MetadataFieldset/validation';
-import { useNameValidation } from '../../shared/NameField';
+import { metadataValidation } from '../../../../common/MetadataFieldset/validation';
+import { useNameValidation } from '../../../../common/NameField';
import { FormValues } from './types';
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx
index b8eeecb80..02a743755 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx
@@ -11,7 +11,7 @@ import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
import { SwitchField } from '@@/form-components/SwitchField';
-import { NameField } from '../../shared/NameField';
+import { NameField } from '../../../../common/NameField';
import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
import { useValidation } from './SocketForm.validation';
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.validation.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.validation.tsx
index f8b48cf0d..04bbb580e 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.validation.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.validation.tsx
@@ -1,7 +1,7 @@
import { boolean, object, SchemaOf, string } from 'yup';
-import { metadataValidation } from '../../shared/MetadataFieldset/validation';
-import { useNameValidation } from '../../shared/NameField';
+import { metadataValidation } from '../../../../common/MetadataFieldset/validation';
+import { useNameValidation } from '../../../../common/NameField';
import { FormValues } from './types';
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardK8sInstall/types.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardK8sInstall/types.ts
new file mode 100644
index 000000000..8ec6d2266
--- /dev/null
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardK8sInstall/types.ts
@@ -0,0 +1,21 @@
+import { K8sDistributionType, KaasProvider } from '../../../types';
+
+export type ProvisionOption = KaasProvider | K8sDistributionType;
+
+export const providerTitles: Record = {
+ civo: 'Civo',
+ linode: 'Linode',
+ digitalocean: 'DigitalOcean',
+ gke: 'Google Cloud',
+ amazon: 'AWS',
+ azure: 'Azure',
+};
+
+export const k8sInstallTitles: Record = {
+ microk8s: 'MicroK8s',
+};
+
+export const provisionOptionTitles: Record = {
+ ...providerTitles,
+ ...k8sInstallTitles,
+};
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx
index 91c9cf630..a930c90fc 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx
@@ -9,7 +9,7 @@ import { CreateAgentEnvironmentValues } from '@/react/portainer/environments/env
import { LoadingButton } from '@@/buttons/LoadingButton';
-import { NameField } from '../NameField';
+import { NameField } from '../../../../common/NameField';
import { MoreSettingsSection } from '../MoreSettingsSection';
import { EnvironmentUrlField } from './EnvironmentUrlField';
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.validation.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.validation.tsx
index 4829d9bbf..90eae2bcb 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.validation.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.validation.tsx
@@ -2,8 +2,8 @@ import { object, SchemaOf, string } from 'yup';
import { CreateAgentEnvironmentValues } from '@/react/portainer/environments/environment.service/create';
-import { metadataValidation } from '../MetadataFieldset/validation';
-import { useNameValidation } from '../NameField';
+import { metadataValidation } from '../../../../common/MetadataFieldset/validation';
+import { useNameValidation } from '../../../../common/NameField';
export function useValidation(): SchemaOf {
return object({
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentFieldset.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentFieldset.tsx
index a85bf0c49..b95c2be60 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentFieldset.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentFieldset.tsx
@@ -2,7 +2,7 @@ import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { PortainerTunnelAddrField } from '@/react/portainer/common/PortainerTunnelAddrField';
import { PortainerUrlField } from '@/react/portainer/common/PortainerUrlField';
-import { NameField } from '../../NameField';
+import { NameField } from '../../../../../common/NameField';
interface EdgeAgentFormProps {
readonly?: boolean;
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts
index 09fb72517..8f61ba562 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts
@@ -7,8 +7,8 @@ import {
import { validation as urlValidation } from '@/react/portainer/common/PortainerTunnelAddrField';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
-import { metadataValidation } from '../../MetadataFieldset/validation';
-import { useNameValidation } from '../../NameField';
+import { metadataValidation } from '../../../../../common/MetadataFieldset/validation';
+import { useNameValidation } from '../../../../../common/NameField';
import { FormValues } from './types';
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MoreSettingsSection.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MoreSettingsSection.tsx
index 1e1427c7e..9deb8d048 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MoreSettingsSection.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MoreSettingsSection.tsx
@@ -2,7 +2,7 @@ import { PropsWithChildren } from 'react';
import { FormSection } from '@@/form-components/FormSection';
-import { MetadataFieldset } from './MetadataFieldset';
+import { MetadataFieldset } from '../../../common/MetadataFieldset';
export function MoreSettingsSection({ children }: PropsWithChildren) {
return (
diff --git a/app/react/portainer/registries/types/registry.ts b/app/react/portainer/registries/types/registry.ts
index 6bcd7bcf0..3f134be1d 100644
--- a/app/react/portainer/registries/types/registry.ts
+++ b/app/react/portainer/registries/types/registry.ts
@@ -1,5 +1,5 @@
import { TeamId } from '@/react/portainer/users/teams/types';
-import { UserId } from '@/portainer/users/types';
+import { UserId } from '@/portainer/users/types/user-id';
import { TLSConfiguration } from '../../settings/types';
@@ -20,12 +20,12 @@ export enum RegistryTypes {
}
export type RoleId = number;
-interface AccessPolicy {
+export interface AccessPolicy {
RoleId: RoleId;
}
-type UserAccessPolicies = Record; // map[UserID]AccessPolicy
-type TeamAccessPolicies = Record;
+export type UserAccessPolicies = Record; // map[UserID]AccessPolicy
+export type TeamAccessPolicies = Record;
export interface RegistryAccess {
UserAccessPolicies: UserAccessPolicies;