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
- -
-
- - You can find out more information about how to protect a Docker environment with TLS in the - Docker documentation. - -
-
- - - -
Required TLS files
- -
- -
- -
- - - {{ $ctrl.formData.TLSCACert.name }} - - - -
-
- - -
- -
- -
- - - {{ $ctrl.formData.TLSCert.name }} - - - -
-
- - -
- -
- - - {{ $ctrl.formData.TLSKey.name }} - - - -
-
- -
- -
- -
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. - - -
- -
-
- - -
-
Configuration
- -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- -
-
Check-in Intervals
- -
- - - - - - -
Metadata
- -
- -
- -
-
- - - - - -
-
Open Active Management Technology
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
-
- -
-
- - Cancel -
-
-
-
-
-
-
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 ( +
+
+ + + + + + + + + + + {!!info.Region && ( + + + + + )} + {!!info.Size && ( + + + + + )} + {!!info.NetworkID && ( + + + + + )} + {!!info.NodeIPs && ( + + + + + )} + +
Provider + {info.Name} + + + + +
Region{info.Region}
Node Size{info.Size}
Network Id{info.NetworkID}
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 }) => ( +
+ + + {!isErrorState && ( + <> + {!isEdge && + (isAgent ? ( + + ) : ( + + ))} + {!isAzure && } + + {isEdge && isBE && ( + + Use https connection on Edge agent to use private + registries with credentials. + + )} + + )} + + {isEdge && ( + + {environment.Edge.AsyncMode ? ( + + setValues((values) => ({ + ...values, + edge: { ...values.edge, ...value }, + })) + } + /> + ) : ( + + setFieldValue('edge.checkInInterval', value) + } + /> + )} + + )} + {!isEdge && + environment.Status !== EnvironmentStatus.Error && + isDockerAPIEnvironment(environment) && ( + + setValues((values) => ({ + ...values, + tlsConfig: { ...values.tlsConfig, ...tlsConfig }, + })) + } + /> + )} + {isAzure && ( + + setFieldValue(`azure.${field}`, value) + } + values={values.azure} + /> + )} + + {isAmtVisible && } + + + + + )} +
+
+
+
+
+ ); +} 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;