mirror of
https://github.com/portainer/portainer.git
synced 2025-08-10 08:15:25 +02:00
refactor(environments): migrate item view to react [EE-2300]
fix [EE-2300]
This commit is contained in:
parent
7549b6cf3f
commit
b0bfbc3ad7
70 changed files with 1651 additions and 1092 deletions
|
@ -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), [
|
||||
|
|
|
@ -194,8 +194,7 @@ angular
|
|||
},
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/endpoints/edit/endpoint.html',
|
||||
controller: 'EndpointController',
|
||||
component: 'environmentsItemView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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: '<',
|
||||
},
|
||||
});
|
|
@ -1,83 +0,0 @@
|
|||
<div>
|
||||
<!-- tls-checkbox -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
label="'TLS'"
|
||||
label-class="'col-sm-2'"
|
||||
checked="$ctrl.formData.TLS"
|
||||
on-change="($ctrl.onToggleTLS)"
|
||||
tooltip="'Enable this option if you need to connect to the Docker environment with TLS.'"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tls-checkbox -->
|
||||
<div class="col-sm-12 form-section-title" ng-if="$ctrl.formData.TLS"> TLS mode </div>
|
||||
<!-- note -->
|
||||
<div class="form-group" ng-if="$ctrl.formData.TLS">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">
|
||||
You can find out more information about how to protect a Docker environment with TLS in the
|
||||
<a href="https://docs.docker.com/engine/security/https/" target="_blank">Docker documentation</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<box-selector
|
||||
ng-if="$ctrl.formData.TLS"
|
||||
slim="true"
|
||||
radio-name="'tls_mode'"
|
||||
options="$ctrl.tlsOptions"
|
||||
value="$ctrl.formData.TLSMode"
|
||||
on-change="($ctrl.onChangeTLSMode)"
|
||||
></box-selector>
|
||||
|
||||
<div class="col-sm-12 form-section-title" ng-if="$ctrl.formData.TLS && $ctrl.formData.TLSMode !== 'tls_only'"> Required TLS files </div>
|
||||
<!-- tls-file-upload -->
|
||||
<div ng-if="$ctrl.formData.TLS">
|
||||
<!-- tls-file-ca -->
|
||||
<div class="form-group" ng-if="$ctrl.formData.TLSMode === 'tls_client_ca' || $ctrl.formData.TLSMode === 'tls_ca'">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left">TLS CA certificate</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<button type="button" class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formData.TLSCACert">Select file</button>
|
||||
<span class="space-left">
|
||||
{{ $ctrl.formData.TLSCACert.name }}
|
||||
<pr-icon icon="'check'" ng-if="$ctrl.formData.TLSCACert && $ctrl.formData.TLSCACert === $ctrl.endpoint.TLSConfig.TLSCACert" mode="'success'"></pr-icon>
|
||||
<pr-icon icon="'x'" ng-if="!$ctrl.formData.TLSCACert" mode="'danger'"></pr-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tls-file-ca -->
|
||||
<!-- tls-files-cert-key -->
|
||||
<div ng-if="$ctrl.formData.TLSMode === 'tls_client_ca' || $ctrl.formData.TLSMode === 'tls_client_noca'">
|
||||
<!-- tls-file-cert -->
|
||||
<div class="form-group">
|
||||
<label for="tls_cert" class="col-sm-3 col-lg-2 control-label text-left">TLS certificate</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<button type="button" class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formData.TLSCert">Select file</button>
|
||||
<span class="space-left">
|
||||
{{ $ctrl.formData.TLSCert.name }}
|
||||
<pr-icon icon="'check'" ng-if="$ctrl.formData.TLSCert && $ctrl.formData.TLSCert === $ctrl.endpoint.TLSConfig.TLSCert" mode="'success'"></pr-icon>
|
||||
<pr-icon icon="'x'" ng-if="!$ctrl.formData.TLSCert" mode="'danger'"></pr-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tls-file-cert -->
|
||||
<!-- tls-file-key -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left">TLS key</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<button type="button" class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formData.TLSKey">Select file</button>
|
||||
<span class="space-left">
|
||||
{{ $ctrl.formData.TLSKey.name }}
|
||||
<pr-icon icon="'check'" ng-if="$ctrl.formData.TLSKey && $ctrl.formData.TLSKey === $ctrl.endpoint.TLSConfig.TLSKey" mode="'success'"></pr-icon>
|
||||
<pr-icon icon="'x'" ng-if="!$ctrl.formData.TLSKey" mode="'danger'"></pr-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tls-file-key -->
|
||||
</div>
|
||||
<!-- tls-files-cert-key -->
|
||||
</div>
|
||||
<!-- !tls-file-upload -->
|
||||
</div>
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
]);
|
|
@ -1,7 +0,0 @@
|
|||
export function EndpointSecurityFormData() {
|
||||
this.TLS = false;
|
||||
this.TLSMode = 'tls_client_ca';
|
||||
this.TLSCACert = null;
|
||||
this.TLSCert = null;
|
||||
this.TLSKey = null;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
);
|
|
@ -1,8 +0,0 @@
|
|||
export const azureEndpointConfig = {
|
||||
bindings: {
|
||||
applicationId: '=',
|
||||
tenantId: '=',
|
||||
authenticationKey: '=',
|
||||
},
|
||||
templateUrl: './azureEndpointConfig.html',
|
||||
};
|
|
@ -1,51 +0,0 @@
|
|||
<div>
|
||||
<div class="col-sm-12 form-section-title"> Azure configuration </div>
|
||||
<!-- applicationId-input -->
|
||||
<div class="form-group">
|
||||
<label for="azure_credential_appid" class="col-sm-3 col-lg-2 control-label text-left">Application ID</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="azure_credential_appid"
|
||||
ng-model="$ctrl.applicationId"
|
||||
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
required
|
||||
data-cy="azure-credential-appid-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !applicationId-input -->
|
||||
<!-- tenantId-input -->
|
||||
<div class="form-group">
|
||||
<label for="azure_credential_tenantid" class="col-sm-3 col-lg-2 control-label text-left">Tenant ID</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="azure_credential_tenantid"
|
||||
ng-model="$ctrl.tenantId"
|
||||
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
required
|
||||
data-cy="azure-credential-tenantid-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tenantId-input -->
|
||||
<!-- authenticationkey-input -->
|
||||
<div class="form-group">
|
||||
<label for="azure_credential_authkey" class="col-sm-3 col-lg-2 control-label text-left">Authentication key</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="azure-credential-authkey-input"
|
||||
class="form-control"
|
||||
name="azure_credential_authkey"
|
||||
ng-model="$ctrl.authenticationKey"
|
||||
placeholder="cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk="
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !authenticationkey-input -->
|
||||
</div>
|
|
@ -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;
|
||||
|
|
|
@ -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<AMTInformation>(
|
||||
`${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,
|
||||
|
|
|
@ -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;
|
||||
|
|
19
app/portainer/react/views/environments.ts
Normal file
19
app/portainer/react/views/environments.ts
Normal file
|
@ -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;
|
|
@ -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))), [])
|
||||
|
|
|
@ -1,268 +0,0 @@
|
|||
<page-header ng-if="endpoint" title="'Environment details'" breadcrumbs="[{label:'Environments', link:'portainer.endpoints'}, endpoint.Name]" reload="true"> </page-header>
|
||||
|
||||
<div class="row">
|
||||
<div ng-if="state.edgeEndpoint">
|
||||
<information-panel ng-if="state.edgeAssociated" title-text="Edge information">
|
||||
<span class="small text-muted">
|
||||
<p class="vertical-center">
|
||||
<pr-icon icon="'alert-circle'" mode="'primary'"></pr-icon>
|
||||
This Edge environment is associated to an Edge environment {{ state.kubernetesEndpoint ? '(Kubernetes)' : '(Docker)' }}.
|
||||
</p>
|
||||
<p>
|
||||
Edge key: <code>{{ endpoint.EdgeKey }}</code>
|
||||
</p>
|
||||
<p>
|
||||
Edge identifier: <code>{{ endpoint.EdgeID }}</code>
|
||||
</p>
|
||||
<p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="state.actionInProgress"
|
||||
ng-click="onDisassociateEndpoint()"
|
||||
button-spinner="state.actionInProgress"
|
||||
analytics-on
|
||||
analytics-event="edge-endpoint-disassociate"
|
||||
analytics-category="edge"
|
||||
>
|
||||
<span ng-hide="state.actionInProgress">Disassociate</span>
|
||||
</button>
|
||||
</p>
|
||||
</span>
|
||||
</information-panel>
|
||||
|
||||
<div class="col-sm-12" ng-if="!state.edgeAssociated">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="col-sm-12 form-section-title">Deploy an agent</div>
|
||||
<span class="small text-muted">
|
||||
<p class="vertical-center">
|
||||
<pr-icon icon="'alert-circle'" mode="'primary'"></pr-icon>
|
||||
Refer to the platform related command below to deploy the Edge agent in your remote cluster.
|
||||
</p>
|
||||
<p>
|
||||
The agent will communicate with Portainer via <u>{{ edgeKeyDetails.instanceURL }}</u> and <u>tcp://{{ edgeKeyDetails.tunnelServerAddr }}</u>
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<div class="col-sm-12 form-section-title"> Edge agent deployment script </div>
|
||||
<edge-script-form edge-info="{ key: endpoint.EdgeKey, id: endpoint.EdgeID }" commands="state.edgeScriptCommands" async-mode="endpoint.Edge.AsyncMode"></edge-script-form>
|
||||
|
||||
<edge-key-display edge-key="endpoint.EdgeKey"> </edge-key-display>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<information-panel ng-if="state.kubernetesEndpoint && (!state.edgeEndpoint || state.edgeAssociated)" title-text="Kubernetes features configuration">
|
||||
<span class="small text-muted vertical-center">
|
||||
<pr-icon icon="'wrench'" mode="'primary'"></pr-icon>
|
||||
You should configure the features available in this Kubernetes environment in the
|
||||
<a ui-sref="kubernetes.cluster.setup({endpointId: endpoint.Id})">Kubernetes configuration</a> view.
|
||||
</span>
|
||||
</information-panel>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="$ctrl.endpointForm">
|
||||
<div class="col-sm-12 form-section-title"> Configuration </div>
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="container-name-input"
|
||||
class="form-control"
|
||||
id="container_name"
|
||||
ng-model="endpoint.Name"
|
||||
placeholder="e.g. kubernetes-cluster01 / docker-prod01"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- endpoint-url-input -->
|
||||
<div class="form-group" ng-if="!state.edgeEndpoint">
|
||||
<label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
<span ng-if="!state.agentEndpoint">Environment URL</span>
|
||||
<span ng-if="state.agentEndpoint">Environment address</span>
|
||||
<portainer-tooltip
|
||||
ng-if="!state.agentEndpoint"
|
||||
message="'URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it.'"
|
||||
>
|
||||
</portainer-tooltip>
|
||||
<portainer-tooltip ng-if="state.agentEndpoint" message="'The address for the Portainer agent in the format <HOST>:<PORT> or <IP>:<PORT>'"> </portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
ng-disabled="endpointType === 'local' || state.azureEndpoint"
|
||||
type="text"
|
||||
data-cy="endpoint-url-input"
|
||||
class="form-control"
|
||||
id="endpoint_url"
|
||||
ng-model="endpoint.URL"
|
||||
placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !endpoint-url-input -->
|
||||
<!-- endpoint-public-url-input -->
|
||||
<div class="form-group" ng-if="!state.azureEndpoint">
|
||||
<label for="endpoint_public_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Public IP
|
||||
<portainer-tooltip message="'URL or IP address where exposed containers will be reachable. This field is optional and will default to the environment URL.'">
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="endpoint_public_url"
|
||||
ng-model="endpoint.PublicURL"
|
||||
placeholder="e.g. 10.0.0.10 or mydocker.mydomain.com"
|
||||
data-cy="public-url-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="endpoint && state.edgeEndpoint">
|
||||
<div class="col-sm-12 form-section-title"> Check-in Intervals </div>
|
||||
<edge-checkin-interval-field value="endpoint.EdgeCheckinInterval" on-change="(onChangeCheckInInterval)"></edge-checkin-interval-field>
|
||||
</div>
|
||||
|
||||
<!-- !endpoint-public-url-input -->
|
||||
|
||||
<tls-fieldset
|
||||
ng-if="!state.edgeEndpoint && endpoint.Status !== 4 && state.showTLSConfig"
|
||||
values="formValues.tlsConfig"
|
||||
on-change="(onChangeTLSConfigFormValues)"
|
||||
validation-data="{optionalCert: true}"
|
||||
></tls-fieldset>
|
||||
|
||||
<azure-endpoint-config
|
||||
ng-if="state.azureEndpoint"
|
||||
application-id="endpoint.AzureCredentials.ApplicationID"
|
||||
tenant-id="endpoint.AzureCredentials.TenantID"
|
||||
authentication-key="endpoint.AzureCredentials.AuthenticationKey"
|
||||
></azure-endpoint-config>
|
||||
<div class="col-sm-12 form-section-title"> Metadata </div>
|
||||
<!-- group -->
|
||||
<div class="form-group">
|
||||
<label for="endpoint_group" class="col-sm-3 col-lg-2 control-label text-left"> Group </label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<select
|
||||
ng-options="group.Id as group.Name for group in groups"
|
||||
ng-model="endpoint.GroupId"
|
||||
id="endpoint_group"
|
||||
class="form-control"
|
||||
data-cy="endpoint-group-select"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !group -->
|
||||
|
||||
<tag-selector ng-if="endpoint" value="endpoint.TagIds" allow-create="state.allowCreate" on-change="(onChangeTags)"></tag-selector>
|
||||
|
||||
<!-- open-amt info -->
|
||||
<div ng-if="state.showAMTInfo">
|
||||
<div class="col-sm-12 form-section-title"> Open Active Management Technology </div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="endpoint_managementinfoVersion" class="col-sm-3 col-lg-2 control-label text-left"> AMT Version </label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
ng-disabled="true"
|
||||
class="form-control"
|
||||
id="endpoint_managementinfoVersion"
|
||||
ng-model="endpoint.ManagementInfo['AMT']"
|
||||
placeholder="Loading..."
|
||||
data-cy="endpoint-managementinfoVersion"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="endpoint_managementinfoUUID" class="col-sm-3 col-lg-2 control-label text-left"> UUID </label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
ng-disabled="true"
|
||||
class="form-control"
|
||||
id="endpoint_managementinfoUUID"
|
||||
ng-model="endpoint.ManagementInfo['UUID']"
|
||||
placeholder="Loading..."
|
||||
data-cy="endpoint-managementinfoUUID"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="endpoint_managementinfoBuildNumber" class="col-sm-3 col-lg-2 control-label text-left"> Build Number </label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="endpoint-managementinfoBuildNumber"
|
||||
ng-disabled="true"
|
||||
class="form-control"
|
||||
id="endpoint_managementinfoBuildNumber"
|
||||
ng-model="endpoint.ManagementInfo['Build Number']"
|
||||
placeholder="Loading..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="endpoint_managementinfoControlMode" class="col-sm-3 col-lg-2 control-label text-left"> Control Mode </label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="endpoint-managementinfoControlMode"
|
||||
ng-disabled="true"
|
||||
class="form-control"
|
||||
id="endpoint_managementinfoControlMode"
|
||||
ng-model="endpoint.ManagementInfo['Control Mode']"
|
||||
placeholder="Loading..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="endpoint_managementinfoDNSSuffix" class="col-sm-3 col-lg-2 control-label text-left"> DNS Suffix </label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="endpoint-managementinfoDNSSuffix"
|
||||
ng-disabled="true"
|
||||
class="form-control"
|
||||
id="endpoint_managementinfoDNSSuffix"
|
||||
ng-model="endpoint.ManagementInfo['DNS Suffix']"
|
||||
placeholder="Loading..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !open-amt info -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm !ml-0"
|
||||
ng-disabled="state.actionInProgress || !endpoint.Name || !endpoint.URL || !$ctrl.endpointForm.$valid"
|
||||
ng-click="updateEndpoint()"
|
||||
button-spinner="state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="state.actionInProgress">Update environment</span>
|
||||
<span ng-show="state.actionInProgress">Updating environment...</span>
|
||||
</button>
|
||||
<a type="button" class="btn btn-default btn-sm" ui-sref="portainer.endpoints">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { FormikErrors } from 'formik';
|
||||
import { SetStateAction } from 'react';
|
||||
|
||||
export function isErrorType<T>(
|
||||
error: string | FormikErrors<T> | undefined
|
||||
|
@ -16,3 +17,17 @@ export function isArrayErrorType<T>(
|
|||
): error is FormikErrors<T>[] {
|
||||
return error !== undefined && typeof error !== 'string';
|
||||
}
|
||||
|
||||
export interface FieldsetValues<TFieldset> {
|
||||
values: TFieldset;
|
||||
errors?: FormikErrors<TFieldset>;
|
||||
}
|
||||
|
||||
export type SetFieldValue<TFieldset> = <TField>(
|
||||
field: keyof TFieldset,
|
||||
value: TField
|
||||
) => void;
|
||||
|
||||
export type SetValues<TFieldset> = SetStateAction<TFieldset>;
|
||||
|
||||
export type OnChange<TFieldset> = (value: TFieldset) => void;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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';
|
||||
|
|
9
app/react/edge/edge-devices/open-amt/query-keys.ts
Normal file
9
app/react/edge/edge-devices/open-amt/query-keys.ts
Normal file
|
@ -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,
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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'),
|
||||
|
|
38
app/react/edge/edge-devices/open-amt/useAmtInfo.ts
Normal file
38
app/react/edge/edge-devices/open-amt/useAmtInfo.ts
Normal file
|
@ -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<AMTInformation>(
|
||||
`/open_amt/${environmentId}/info`
|
||||
);
|
||||
|
||||
return amtInformation;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e, 'Unable to retrieve environment information');
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -461,6 +461,7 @@ function useInitialValues(
|
|||
ingressClasses:
|
||||
getIngressClassesFormValues(allowNoneIngressClass, ingressClasses) ||
|
||||
[],
|
||||
changeWindow: environment.ChangeWindow,
|
||||
};
|
||||
}, [environment, ingressClasses, storageClassFormValues]);
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
};
|
||||
|
|
|
@ -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<ConfigureFormValues> = object({
|
|||
changeWindow: isBE ? endpointChangeWindowSchema.required() : undefined,
|
||||
storageClasses: storageClassFormValuesSchema.required(),
|
||||
ingressClasses: array().of(ingressControllerClassMapSchema).required(),
|
||||
timeZone: string(),
|
||||
deploymentOptions: deploymentOptionsSchema.nullable(),
|
||||
});
|
||||
|
|
113
app/react/kubernetes/cluster/microk8s/addons/addons.service.ts
Normal file
113
app/react/kubernetes/cluster/microk8s/addons/addons.service.ts
Normal file
|
@ -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<string>[];
|
||||
}
|
||||
|
||||
async function getAddons(environmentID: number) {
|
||||
try {
|
||||
const { data } = await axios.get<AddonsResponse>(
|
||||
`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<AddonsResponse>(
|
||||
`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<AddonsResponse>(
|
||||
`cloud/endpoints/${environmentID}/addons`,
|
||||
payload
|
||||
);
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw parseAxiosError(err as Error, 'Unable to update addons');
|
||||
}
|
||||
}
|
||||
|
||||
export function useAddonsQuery<TSelect = AddonsResponse | null>(
|
||||
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')
|
||||
);
|
||||
}
|
32
app/react/kubernetes/cluster/microk8s/addons/types.ts
Normal file
32
app/react/kubernetes/cluster/microk8s/addons/types.ts
Normal file
|
@ -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[];
|
||||
}[];
|
|
@ -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 (
|
||||
<LoadingButton
|
||||
className="!ml-0"
|
||||
loadingText="Disassociating"
|
||||
isLoading={mutation.isLoading}
|
||||
onClick={handleClick}
|
||||
data-cy="disassociate-button"
|
||||
>
|
||||
Disassociate
|
||||
</LoadingButton>
|
||||
);
|
||||
|
||||
async function handleClick() {
|
||||
if (!(await confirmDisassociate())) {
|
||||
return;
|
||||
}
|
||||
|
||||
mutation.mutate(environment.Id, {
|
||||
onSuccess() {
|
||||
notifySuccess('Environment disassociated', environment.Name);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<InformationPanel title="Edge information">
|
||||
<TextTip color="blue">
|
||||
This Edge environment is associated to an Edge environment ({platform}).
|
||||
</TextTip>
|
||||
|
||||
<div className="small text-muted mt-2">
|
||||
<p>
|
||||
Edge key: <code>{environment.EdgeKey}</code>
|
||||
</p>
|
||||
<p>
|
||||
Edge identifier: <code>{environment.EdgeID}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DisassociateButton environment={environment} />
|
||||
</InformationPanel>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<Widget>
|
||||
<Widget.Body>
|
||||
<FormSection title="Deploy an agent">
|
||||
<TextTip>
|
||||
<p className="vertical-center">
|
||||
Refer to the platform related command below to deploy the Edge
|
||||
agent in your remote cluster.
|
||||
</p>
|
||||
<p>
|
||||
The agent will communicate with Portainer via{' '}
|
||||
<u>{edgeKeyDetails.instanceURL}</u> and{' '}
|
||||
<u>tcp://{edgeKeyDetails.tunnelServerAddr}</u>
|
||||
</p>
|
||||
</TextTip>
|
||||
</FormSection>
|
||||
|
||||
<FormSection title="Edge agent deployment script">
|
||||
<EdgeScriptForm
|
||||
edgeInfo={{ key: environment.EdgeKey, id: environment.EdgeID }}
|
||||
commands={{
|
||||
linux: _.compact([
|
||||
commandsTabs.k8sLinux,
|
||||
commandsTabs.swarmLinux,
|
||||
commandsTabs.standaloneLinux,
|
||||
]),
|
||||
win: [commandsTabs.swarmWindows, commandsTabs.standaloneWindow],
|
||||
}}
|
||||
asyncMode={environment.Edge.AsyncMode}
|
||||
/>
|
||||
|
||||
<EdgeKeyDisplay edgeKey={environment.EdgeKey} />
|
||||
</FormSection>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
function decodeEdgeKey(key: string) {
|
||||
if (key === '') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const decodedKey = atob(key).split('|');
|
||||
return {
|
||||
instanceURL: decodedKey[0],
|
||||
tunnelServerAddr: decodedKey[1],
|
||||
};
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { Environment } from '../types';
|
||||
|
||||
import { EdgeDeploymentInfo } from './EdgeDeploymentInfo';
|
||||
import { EdgeAssociationInfo } from './EdgeAssociationInfo';
|
||||
|
||||
export function EdgeEnvironmentDetails({
|
||||
environment,
|
||||
}: {
|
||||
environment: Environment;
|
||||
}) {
|
||||
return (
|
||||
<div className="row">
|
||||
<div>
|
||||
{environment.EdgeID ? (
|
||||
<EdgeAssociationInfo environment={environment} />
|
||||
) : (
|
||||
<EdgeDeploymentInfo environment={environment} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
45
app/react/portainer/environments/ItemView/ItemView.tsx
Normal file
45
app/react/portainer/environments/ItemView/ItemView.tsx
Normal file
|
@ -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 (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Environment details"
|
||||
breadcrumbs={[
|
||||
{ label: 'Environments', link: '^' },
|
||||
environmentQuery.data.Name || '',
|
||||
]}
|
||||
reload
|
||||
/>
|
||||
|
||||
{isEdge && <EdgeEnvironmentDetails environment={environmentQuery.data} />}
|
||||
|
||||
<KubeDetails environment={environmentQuery.data} />
|
||||
|
||||
<div className="mt-4">
|
||||
<UpdateForm environment={environmentQuery.data} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
<Widget>
|
||||
<Widget.Title icon={KubeIcon} title="KaaS cluster details" />
|
||||
|
||||
<Widget.Body className="!p-0">
|
||||
<table className="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Provider</td>
|
||||
<td>
|
||||
{info.Name}
|
||||
|
||||
<span className="ml-2">
|
||||
<Button
|
||||
as="a"
|
||||
size="xsmall"
|
||||
props={{
|
||||
href: info.URL,
|
||||
target: '_blank',
|
||||
rel: 'noreferrer',
|
||||
}}
|
||||
className="ml-2"
|
||||
data-cy="go-to-portal"
|
||||
>
|
||||
Go to portal
|
||||
</Button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{!!info.Region && (
|
||||
<tr>
|
||||
<td>Region</td>
|
||||
<td>{info.Region}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!info.Size && (
|
||||
<tr>
|
||||
<td> Node Size</td>
|
||||
<td>{info.Size}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!info.NetworkID && (
|
||||
<tr>
|
||||
<td>Network Id</td>
|
||||
<td>{info.NetworkID}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!info.NodeIPs && (
|
||||
<tr>
|
||||
<td>Node IPs</td>
|
||||
<td>{info.NodeIPs}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<InformationPanel title="Kubernetes features configuration">
|
||||
<TextTip icon={Wrench} color="blue">
|
||||
You should configure the features available in this Kubernetes
|
||||
environment in the{' '}
|
||||
<Link
|
||||
to="kubernetes.cluster.setup"
|
||||
params={{ endpointId: environmentId }}
|
||||
data-cy="kube-configure-instructions-link"
|
||||
>
|
||||
Kubernetes configuration
|
||||
</Link>{' '}
|
||||
view.
|
||||
</TextTip>
|
||||
</InformationPanel>
|
||||
);
|
||||
}
|
31
app/react/portainer/environments/ItemView/KubeDetails.tsx
Normal file
31
app/react/portainer/environments/ItemView/KubeDetails.tsx
Normal file
|
@ -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 && (
|
||||
<KubeConfigureInstructions environmentId={environment.Id} />
|
||||
)}
|
||||
|
||||
{environment.CloudProvider?.Name.toLowerCase() ===
|
||||
k8sInstallTitles.microk8s.toLowerCase() ? (
|
||||
<Microk8sClusterDetails environmentId={environment.Id} />
|
||||
) : (
|
||||
environment.CloudProvider?.URL && (
|
||||
<KaaSClusterDetails info={environment.CloudProvider} />
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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 <TextTip color="orange">Unable to load environment</TextTip>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Widget>
|
||||
<WidgetTitle icon={Kube} title="MicroK8s cluster details" />
|
||||
<WidgetBody loading={addonsQuery.isLoading || nodesQuery.isLoading}>
|
||||
<DetailsTable dataCy="microk8s-cluster-details-table">
|
||||
<DetailsTable.Row label="Addons" colClassName="w-1/2">
|
||||
{addonsQuery.isError && 'Unable to get addons'}
|
||||
{!addonNames?.length &&
|
||||
addonsQuery.isSuccess &&
|
||||
'No addons installed'}
|
||||
{addonNames?.length && addonNames.join(', ')}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Kubernetes version" colClassName="w-1/2">
|
||||
{addonsQuery.isError && 'Unable to find kubernetes version'}
|
||||
{!!currentVersion && currentVersion}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Node count" colClassName="w-1/2">
|
||||
{nodesQuery.isError && 'Unable to get node count'}
|
||||
{nodes && nodes.length}
|
||||
</DetailsTable.Row>
|
||||
</DetailsTable>
|
||||
<TextTip color="blue">
|
||||
You can{' '}
|
||||
<Link
|
||||
to="kubernetes.cluster"
|
||||
params={{ endpointId: environmentId }}
|
||||
data-cy="cluster-details-view-link"
|
||||
>
|
||||
manage the cluster
|
||||
</Link>{' '}
|
||||
to upgrade, add/remove nodes or enable/disable addons.
|
||||
</TextTip>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<FormSection title="Open Active Management Technology">
|
||||
<FormControl
|
||||
label="AMT Version"
|
||||
inputId="endpoint_management_info_version"
|
||||
>
|
||||
<Input
|
||||
id="endpoint_management_info_version"
|
||||
value={info?.amt}
|
||||
disabled
|
||||
placeholder="Loading..."
|
||||
data-cy="endpoint-managementInfoVersion"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl label="UUID" inputId="endpoint_management_info_uuid">
|
||||
<Input
|
||||
id="endpoint_management_info_uuid"
|
||||
value={info?.uuid}
|
||||
disabled
|
||||
placeholder="Loading..."
|
||||
data-cy="endpoint-managementInfoUUID"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="Build Number"
|
||||
inputId="endpoint_management_info_build_number"
|
||||
>
|
||||
<Input
|
||||
id="endpoint_management_info_build_number"
|
||||
value={info?.buildNumber}
|
||||
disabled
|
||||
placeholder="Loading..."
|
||||
data-cy="endpoint-managementInfoBuildNumber"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="Control Mode"
|
||||
inputId="endpoint_management_info_control_mode"
|
||||
>
|
||||
<Input
|
||||
id="endpoint_management_info_control_mode"
|
||||
value={info?.controlMode}
|
||||
disabled
|
||||
placeholder="Loading..."
|
||||
data-cy="endpoint-managementInfoControlMode"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="DNS Suffix"
|
||||
inputId="endpoint_management_info_dns_suffix"
|
||||
>
|
||||
<Input
|
||||
id="endpoint_management_info_dns_suffix"
|
||||
value={info?.dnsSuffix}
|
||||
disabled
|
||||
placeholder="Loading..."
|
||||
data-cy="endpoint-managementInfoDNSSuffix"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormSection>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<FormControl
|
||||
label="Environment address"
|
||||
tooltip="The address for the Portainer agent in the format <HOST>:<PORT> or <IP>:<PORT>"
|
||||
inputId="endpoint_url"
|
||||
errors={error}
|
||||
>
|
||||
<Input
|
||||
id="endpoint_url"
|
||||
name="url"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375"
|
||||
data-cy="url-input"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
|
@ -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<AzureFormValues> & {
|
||||
setFieldValue: SetFieldValue<AzureFormValues>;
|
||||
}) {
|
||||
return (
|
||||
<FormSection title="Azure Configuration">
|
||||
<FormControl
|
||||
label="Application ID"
|
||||
inputId="azure_application_id"
|
||||
errors={errors?.applicationId}
|
||||
>
|
||||
<Input
|
||||
id="azure_application_id"
|
||||
name="azure.applicationId"
|
||||
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
value={values.applicationId}
|
||||
onChange={(e) => setFieldValue('applicationId', e.target.value)}
|
||||
data-cy="azure-credential-appid-input"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Tenant ID"
|
||||
inputId="azure_tenant_id"
|
||||
errors={errors?.tenantId}
|
||||
>
|
||||
<Input
|
||||
id="azure_tenant_id"
|
||||
name="azure.tenantId"
|
||||
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
value={values.tenantId}
|
||||
onChange={(e) => setFieldValue('tenantId', e.target.value)}
|
||||
data-cy="azure-credential-tenantid-input"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Authentication key"
|
||||
inputId="azure_auth_key"
|
||||
errors={errors?.authKey}
|
||||
>
|
||||
<Input
|
||||
id="azure_auth_key"
|
||||
name="azure.authKey"
|
||||
placeholder="cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk="
|
||||
value={values.authKey}
|
||||
onChange={(e) => setFieldValue('authKey', e.target.value)}
|
||||
data-cy="azure-credential-authkey-input"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormSection>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<FormControl
|
||||
label="Public IP"
|
||||
tooltip="URL or IP address where exposed containers will be reachable. This field is optional and will default to the environment URL."
|
||||
inputId="endpoint_public_url"
|
||||
errors={error}
|
||||
>
|
||||
<Input
|
||||
id="endpoint_public_url"
|
||||
name="publicUrl"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="e.g. 10.0.0.10 or mydocker.mydomain.com"
|
||||
data-cy="public-url-input"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<FormControl
|
||||
label="Environment URL"
|
||||
tooltip="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."
|
||||
inputId="endpoint_url"
|
||||
errors={error}
|
||||
>
|
||||
<Input
|
||||
disabled={disabled}
|
||||
name="url"
|
||||
id="endpoint_url"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375"
|
||||
data-cy="url-input"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Widget>
|
||||
<Widget.Body>
|
||||
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||
{({ values, errors, setFieldValue, setValues, isValid }) => (
|
||||
<Form className="form-horizontal">
|
||||
<FormSection title="Configuration">
|
||||
<NameField disabled={isErrorState} />
|
||||
{!isErrorState && (
|
||||
<>
|
||||
{!isEdge &&
|
||||
(isAgent ? (
|
||||
<AgentAddressField />
|
||||
) : (
|
||||
<URLField disabled={isAzure || isLocal} />
|
||||
))}
|
||||
{!isAzure && <PublicIPField />}
|
||||
|
||||
{isEdge && isBE && (
|
||||
<TextTip color="blue">
|
||||
Use https connection on Edge agent to use private
|
||||
registries with credentials.
|
||||
</TextTip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</FormSection>
|
||||
{isEdge && (
|
||||
<FormSection title="Check-in Intervals">
|
||||
{environment.Edge.AsyncMode ? (
|
||||
<EdgeAsyncIntervalsForm
|
||||
values={values.edge}
|
||||
onChange={(value) =>
|
||||
setValues((values) => ({
|
||||
...values,
|
||||
edge: { ...values.edge, ...value },
|
||||
}))
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<EdgeCheckinIntervalField
|
||||
value={values.edge.checkInInterval || 0}
|
||||
onChange={(value) =>
|
||||
setFieldValue('edge.checkInInterval', value)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</FormSection>
|
||||
)}
|
||||
{!isEdge &&
|
||||
environment.Status !== EnvironmentStatus.Error &&
|
||||
isDockerAPIEnvironment(environment) && (
|
||||
<TLSFieldset
|
||||
errors={errors.tlsConfig}
|
||||
values={values.tlsConfig}
|
||||
onChange={(tlsConfig) =>
|
||||
setValues((values) => ({
|
||||
...values,
|
||||
tlsConfig: { ...values.tlsConfig, ...tlsConfig },
|
||||
}))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isAzure && (
|
||||
<AzureEnvironmentConfiguration
|
||||
errors={errors.azure}
|
||||
setFieldValue={(field, value) =>
|
||||
setFieldValue(`azure.${field}`, value)
|
||||
}
|
||||
values={values.azure}
|
||||
/>
|
||||
)}
|
||||
<MetadataFieldset />
|
||||
{isAmtVisible && <AmtInfo environmentId={environment.Id} />}
|
||||
<FormActions
|
||||
isLoading={isLoading}
|
||||
isValid={isValid}
|
||||
loadingText="Updating environment..."
|
||||
submitLabel="Update environment"
|
||||
data-cy="update-environment-button"
|
||||
>
|
||||
<Button
|
||||
as={Link}
|
||||
props={{
|
||||
to: '^',
|
||||
'data-cy': 'cancel-update-environment-button',
|
||||
}}
|
||||
color="default"
|
||||
data-cy="cancel-update-environment-button"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 || '^');
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import { Shield } from 'lucide-react';
|
||||
|
||||
import { BoxSelectorOption } from '@@/BoxSelector';
|
||||
|
||||
export const tlsOptions: ReadonlyArray<BoxSelectorOption<string>> = [
|
||||
{
|
||||
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;
|
|
@ -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({
|
|||
>
|
||||
<Input
|
||||
id={id}
|
||||
data-cy="environmentCreate-nameInput"
|
||||
data-cy="name-input"
|
||||
name="name"
|
||||
onChange={(e) => setDebouncedValue(e.target.value)}
|
||||
value={debouncedValue}
|
||||
placeholder={placeholder}
|
||||
readOnly={readonly}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
|
@ -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));
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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<Environment> {
|
||||
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({
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { K8sDistributionType, KaasProvider } from '../../../types';
|
||||
|
||||
export type ProvisionOption = KaasProvider | K8sDistributionType;
|
||||
|
||||
export const providerTitles: Record<KaasProvider, string> = {
|
||||
civo: 'Civo',
|
||||
linode: 'Linode',
|
||||
digitalocean: 'DigitalOcean',
|
||||
gke: 'Google Cloud',
|
||||
amazon: 'AWS',
|
||||
azure: 'Azure',
|
||||
};
|
||||
|
||||
export const k8sInstallTitles: Record<K8sDistributionType, string> = {
|
||||
microk8s: 'MicroK8s',
|
||||
};
|
||||
|
||||
export const provisionOptionTitles: Record<ProvisionOption, string> = {
|
||||
...providerTitles,
|
||||
...k8sInstallTitles,
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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<CreateAgentEnvironmentValues> {
|
||||
return object({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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<unknown>) {
|
||||
return (
|
||||
|
|
|
@ -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<UserId, AccessPolicy>; // map[UserID]AccessPolicy
|
||||
type TeamAccessPolicies = Record<TeamId, AccessPolicy>;
|
||||
export type UserAccessPolicies = Record<UserId, AccessPolicy>; // map[UserID]AccessPolicy
|
||||
export type TeamAccessPolicies = Record<TeamId, AccessPolicy>;
|
||||
|
||||
export interface RegistryAccess {
|
||||
UserAccessPolicies: UserAccessPolicies;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue