1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-04 21:35:23 +02:00

chore(fdo): remove FDO code EE-7235 (#11981)

This commit is contained in:
andres-portainer 2024-06-28 08:42:16 -03:00 committed by GitHub
parent 1a3db327c7
commit 19fa40286a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 3 additions and 2609 deletions

View file

@ -200,17 +200,6 @@ angular
},
};
var deviceImport = {
name: 'portainer.endpoints.importDevice',
url: '/device',
views: {
'content@': {
templateUrl: './views/devices/import/importDevice.html',
controller: 'ImportDeviceController',
},
},
};
const edgeAutoCreateScript = {
name: 'portainer.endpoints.edgeAutoCreateScript',
url: '/aeec',
@ -224,26 +213,6 @@ angular
},
};
var addFDOProfile = {
name: 'portainer.endpoints.profile',
url: '/profile',
views: {
'content@': {
component: 'addProfileView',
},
},
};
var editFDOProfile = {
name: 'portainer.endpoints.profile.edit',
url: '/:id',
views: {
'content@': {
component: 'editProfileView',
},
},
};
var endpointAccess = {
name: 'portainer.endpoints.endpoint.access',
url: '/access',
@ -484,9 +453,6 @@ angular
$stateRegistryProvider.register(endpointAccess);
$stateRegistryProvider.register(endpointKVM);
$stateRegistryProvider.register(edgeAutoCreateScript);
$stateRegistryProvider.register(deviceImport);
$stateRegistryProvider.register(addFDOProfile);
$stateRegistryProvider.register(editFDOProfile);
$stateRegistryProvider.register(groups);
$stateRegistryProvider.register(group);
$stateRegistryProvider.register(groupAccess);

View file

@ -1,99 +0,0 @@
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { FDOConfiguration, DeviceConfiguration, Profile } from './model';
const BASE_URL = '/fdo';
export async function configureFDO(formValues: FDOConfiguration) {
try {
await axios.post(`${BASE_URL}/configure`, formValues);
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to configure FDO');
}
}
export async function configureDevice(
deviceId: string,
deviceConfig: DeviceConfiguration
) {
try {
await axios.post(`${BASE_URL}/configure/${deviceId}`, deviceConfig);
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to configure device');
}
}
export async function createProfile(
name: string,
method: string,
profileFileContent: string
) {
const payload = {
name,
profileFileContent,
};
try {
await axios.post(`${BASE_URL}/profiles`, payload, {
params: { method },
});
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to create profile');
}
}
export async function getProfiles() {
try {
const { data: profiles } = await axios.get<Profile[]>(
`${BASE_URL}/profiles`
);
return profiles;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to retrieve the profiles');
}
}
export async function getProfile(profileId: number) {
try {
const { data: profile } = await axios.get<Profile>(
`${BASE_URL}/profiles/${profileId}`
);
return profile;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to retrieve profile');
}
}
export async function deleteProfile(profileId: number) {
try {
await axios.delete(`${BASE_URL}/profiles/${profileId}`);
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to delete profile');
}
}
export async function updateProfile(
id: number,
name: string,
profileFileContent: string
) {
const payload = {
name,
profileFileContent,
};
try {
await axios.put(`${BASE_URL}/profiles/${id}`, payload);
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to update profile');
}
}
export async function duplicateProfile(id: number) {
try {
const { data: profile } = await axios.post<Profile>(
`${BASE_URL}/profiles/${id}/duplicate`
);
return profile;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to duplicate profile');
}
}

View file

@ -1,20 +0,0 @@
export interface FDOConfiguration {
enabled: boolean;
ownerURL: string;
ownerUsername: string;
ownerPassword: string;
}
export interface DeviceConfiguration {
edgeID: string;
edgeKey: string;
name: string;
profile: string;
}
export type Profile = {
id: number;
name: string;
fileContent: string;
dateCreated: string;
};

View file

@ -6,7 +6,6 @@ export function SettingsViewModel(data) {
this.LDAPSettings = data.LDAPSettings;
this.OAuthSettings = new OAuthSettingsViewModel(data.OAuthSettings);
this.openAMTConfiguration = data.openAMTConfiguration;
this.fdoConfiguration = data.fdoConfiguration;
this.SnapshotInterval = data.SnapshotInterval;
this.TemplatesURL = data.TemplatesURL;
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;
@ -37,7 +36,6 @@ export function PublicSettingsViewModel(settings) {
this.Edge = new EdgeSettingsViewModel(settings.Edge);
this.DefaultRegistry = settings.DefaultRegistry;
this.IsAMTEnabled = settings.IsAMTEnabled;
this.IsFDOEnabled = settings.IsFDOEnabled;
}
export function InternalAuthSettingsViewModel(data) {

View file

@ -1,6 +1,5 @@
import angular from 'angular';
import { SettingsFDO } from '@/react/portainer/settings/EdgeComputeView/SettingsFDO';
import { SettingsOpenAMT } from '@/react/portainer/settings/EdgeComputeView/SettingsOpenAMT';
import { InternalAuth } from '@/react/portainer/settings/AuthenticationView/InternalAuth';
import { r2a } from '@/react-tools/react2angular';
@ -17,10 +16,6 @@ import { AuthStyleField } from '@/react/portainer/settings/AuthenticationView/OA
export const settingsModule = angular
.module('portainer.app.react.components.settings', [])
.component(
'settingsFdo',
r2a(withUIRouter(withReactQuery(SettingsFDO)), ['onSubmit', 'settings'])
)
.component('settingsOpenAmt', r2a(SettingsOpenAMT, ['onSubmit', 'settings']))
.component(
'internalAuth',

View file

@ -195,15 +195,5 @@ function FileUploadFactory($q, Upload) {
return $q.all(queue);
};
service.uploadOwnershipVoucher = function (voucherFile) {
return Upload.upload({
url: 'api/fdo/register',
data: {
voucher: voucherFile,
},
ignoreLoadingBar: true,
});
};
return service;
}

View file

@ -1,237 +0,0 @@
<page-header title="'FDO Device Configuration'" breadcrumbs="[{label:'Environments', link:'portainer.endpoints'}, 'Import FDO Device']" reload="true"> </page-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="wand-2" title-text="Import Device Set up"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal" name="fdoForm">
<!-- info -->
<span class="small">
<p class="text-muted" style="margin-top: 10px">
<pr-icon icon="'info'" mode="'primary'" class-name="'mr-0.5'"></pr-icon>
You are setting up a Portainer Edge Agent that will initiate the communications with the Portainer instance and your FDO Devices.
</p>
</span>
<!-- !info -->
<!-- import voucher -->
<div class="col-sm-12 form-section-title"> Import Voucher </div>
<div>
<div class="form-group" ng-show="!state.vouchersUploaded">
<span class="small col-sm-12">
<p class="text-muted" style="margin-top: 10px">
<pr-icon icon="'info'" mode="'primary'" class-name="'mr-0.5'"></pr-icon>
Import one or more Manufacturer's Ownership Vouchers to initiate device attestation
</p>
</span>
<div class="col-sm-8">
<button
style="margin-left: 0px !important"
class="btn btn-sm btn-primary"
ngf-select="onVoucherFilesChange()"
ng-model="formValues.VoucherFiles"
name="VoucherFiles"
ng-disabled="state.vouchersUploading"
button-spinner="state.vouchersUploading"
multiple
>
<span ng-hide="state.vouchersUploading"
>Upload
<pr-icon icon="'upload'" class-name="'ml-1'"></pr-icon>
</span>
<span ng-show="state.vouchersUploading">Uploading Voucher...</span>
</button>
</div>
</div>
<div class="form-group" ng-show="state.vouchersUploading">
<div class="col-sm-12 small text-success">
<p>Connecting to the Owner service...</p>
</div>
</div>
<div class="form-group" ng-show="state.vouchersUploaded">
<div class="col-sm-12">
<p>Ownership Voucher Uploaded <pr-icon icon="'check'" mode="'success'"></pr-icon></p>
</div>
</div>
</div>
<!-- !import voucher -->
<!-- device details -->
<div class="col-sm-12 form-section-title"> Device details </div>
<div>
<span class="small">
<p class="text-muted" style="margin-top: 10px">
<pr-icon icon="'info'" mode="'primary'" class-name="'mr-0.5'"></pr-icon>
Device name will serve as your reference name in Portainer
</p>
</span>
<!-- device name input -->
<div class="form-group">
<label for="device_name" class="col-sm-3 col-lg-2 control-label text-left">Device Name</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
data-cy="deviceImport-deviceNameInput"
class="form-control"
name="device_name"
placeholder="e.g. FDO-Test01"
ng-model="formValues.DeviceName"
ng-required="state.vouchersUploaded"
ng-disabled="!state.vouchersUploaded"
auto-focus
/>
</div>
</div>
<div class="form-group" ng-show="fdoForm.device_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="fdoForm.device_name.$error">
<p ng-message="required">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
This field is required.</p
>
</div>
</div>
</div>
<!-- !device name input -->
<!-- suffix input -->
<span class="small">
<p class="text-muted" style="margin-top: 10px">
<pr-icon icon="'info'" mode="'primary'" class-name="'mr-0.5'"></pr-icon>
Suffix starting number will be appended to the end of the Device name, if initiating multiple devices this will be incrementally increased
</p>
</span>
<div class="form-group">
<label for="suffix" class="col-sm-3 col-lg-2 control-label text-left"> Suffix starting number </label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
data-cy="deviceImport-suffixInput"
class="form-control"
name="suffix"
ng-model="formValues.Suffix"
ng-required="state.vouchersUploaded"
ng-disabled="!state.vouchersUploaded"
ng-pattern="/^[0-9]+$/"
placeholder="1"
required
/>
</div>
</div>
<div class="form-group" ng-show="fdoForm.suffix.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="fdoForm.suffix.$error">
<p ng-message="required">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
This field is required.</p
>
<p ng-message="pattern">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
This field needs to be a positive integer number.</p
>
</div>
</div>
</div>
<!-- !suffix input -->
<!-- portainer-instance-input -->
<div class="form-group">
<label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left">
Portainer server URL
<portainer-tooltip message="'URL of the Portainer instance that the agent will use to initiate the communications.'"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
data-cy="deviceImport-portainerServerUrlInput"
class="form-control"
name="endpoint_url"
ng-model="formValues.PortainerURL"
ng-required="state.vouchersUploaded"
ng-disabled="!state.vouchersUploaded"
placeholder="e.g. https://10.0.0.10:9443"
required
/>
</div>
</div>
<div class="form-group" ng-show="fdoForm.endpoint_url.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="fdoForm.endpoint_url.$error">
<p ng-message="required">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
This field is required.</p
>
</div>
</div>
</div>
<!-- !portainer-instance-input -->
</div>
<!-- device profile input -->
<div class="form-group">
<label for="device_profile" class="col-sm-3 col-lg-2 control-label text-left">Device Profile</label>
<div class="col-sm-9 col-lg-10">
<select
id="device_profile"
data-cy="deviceImport-deviceProfileSelect"
ng-model="formValues.DeviceProfile"
class="form-control"
ng-required="state.vouchersUploaded"
ng-disabled="!state.vouchersUploaded"
>
<option selected disabled hidden value="">Select a profile for your device</option>
<option ng-repeat="profile in profiles | orderBy: 'name'" ng-value="profile.id">{{ profile.name }}</option>
</select>
</div>
</div>
<!-- !device profile input -->
<!-- !device details -->
<!-- tags -->
<div class="col-sm-12 form-section-title"> Set up Tags </div>
<div>
<span class="small">
<p class="text-muted" style="margin-top: 10px">
<pr-icon icon="'info'" mode="'primary'" class-name="'mr-0.5'"></pr-icon>
This is just an option if your device is under a certain group
</p>
</span>
<!-- group -->
<div class="form-group">
<label for="device_group" class="col-sm-3 col-lg-2 control-label text-left"> Group </label>
<div class="col-sm-9 col-lg-10">
<select
class="form-control"
data-cy="deviceImport-deviceGroupSelect"
ng-options="group.Id as group.Name for group in groups"
ng-model="formValues.GroupId"
id="device_group"
ng-required="state.vouchersUploaded"
ng-disabled="!state.vouchersUploaded"
data-cy="deviceImport-deviceGroup"
></select>
</div>
</div>
<!-- !group -->
<tag-selector ng-if="formValues" value="formValues.TagIds" allow-create="state.allowCreateTag" on-change="(onChangeTags)"> </tag-selector>
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-click="createEndpointAndConfigureDevice()"
ng-disabled="state.actionInProgress || !state.vouchersUploaded || !fdoForm.$valid"
button-spinner="state.actionInProgress"
data-cy="deviceImport-saveDeviceButton"
>
<span ng-hide="state.actionInProgress">Save Configuration</span>
<span ng-show="state.actionInProgress">Saving...</span>
</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="portainer.endpoints">Cancel</a>
</div>
</div>
<!-- !actions -->
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -1,138 +0,0 @@
import uuidv4 from 'uuid/v4';
import { PortainerEndpointCreationTypes } from 'Portainer/models/endpoint/models';
import { configureDevice, getProfiles } from 'Portainer/hostmanagement/fdo/fdo.service';
angular
.module('portainer.app')
.controller(
'ImportDeviceController',
function ImportDeviceController($async, $q, $scope, $state, EndpointService, GroupService, TagService, Notifications, Authentication, FileUploadService) {
$scope.state = {
actionInProgress: false,
vouchersUploading: false,
vouchersUploaded: false,
deviceIDs: [],
allowCreateTag: Authentication.isAdmin(),
};
$scope.formValues = {
DeviceName: '',
DeviceProfile: '',
GroupId: 1,
TagIds: [],
VoucherFiles: [],
PortainerURL: '',
Suffix: 1,
};
$scope.profiles = [];
$scope.onChangeTags = function onChangeTags(value) {
return $scope.$evalAsync(() => {
$scope.formValues.TagIds = value;
});
};
$scope.onVoucherFilesChange = function () {
if ($scope.formValues.VoucherFiles.length < 1) {
return;
}
$scope.state.vouchersUploading = true;
let uploads = $scope.formValues.VoucherFiles.map((f) => FileUploadService.uploadOwnershipVoucher(f));
$q.all(uploads)
.then(function success(responses) {
$scope.state.vouchersUploading = false;
$scope.state.vouchersUploaded = true;
$scope.state.deviceIDs = responses.map((r) => r.data.guid);
})
.catch(function error(err) {
$scope.state.vouchersUploading = false;
if ($scope.formValues.VoucherFiles.length === 1) {
Notifications.error('Failure', err, 'Unable to upload the Ownership Voucher');
} else {
Notifications.error('Failure', null, 'Unable to upload the Ownership Vouchers, please check the logs');
}
});
};
$scope.createEndpointAndConfigureDevice = function () {
return $async(async () => {
$scope.state.actionInProgress = true;
let suffix = $scope.formValues.Suffix;
for (const deviceID of $scope.state.deviceIDs) {
let deviceName = $scope.formValues.DeviceName + suffix;
try {
var endpoint = await EndpointService.createRemoteEndpoint(
deviceName,
PortainerEndpointCreationTypes.EdgeAgentEnvironment,
$scope.formValues.PortainerURL,
'',
$scope.formValues.GroupId,
$scope.formValues.TagIds,
false,
false,
false,
null,
null,
null,
null
);
} catch (err) {
Notifications.error('Failure', err, 'Unable to create the environment');
$scope.state.actionInProgress = false;
return;
}
suffix++;
const config = {
edgeID: endpoint.EdgeID || uuidv4(),
edgeKey: endpoint.EdgeKey,
name: deviceName,
profile: $scope.formValues.DeviceProfile,
};
try {
await configureDevice(deviceID, config);
} catch (err) {
Notifications.error('Failure', err, 'Unable to import device');
return;
} finally {
$scope.state.actionInProgress = false;
}
}
Notifications.success('Success', 'Device(s) successfully imported');
$state.go('edge.devices');
});
};
async function initView() {
try {
$scope.profiles = await getProfiles();
} catch (err) {
Notifications.error('Failure', err, 'Unable to load profiles');
return;
}
$q.all({
groups: GroupService.groups(),
})
.then(function success(data) {
$scope.groups = data.groups;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to load groups');
});
}
initView();
}
);

View file

@ -1,65 +0,0 @@
<page-header title="'Create profile'" breadcrumbs="[{label:'Settings', link:'portainer.settings'}, 'Edge Compute']" reload="true"> </page-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="createProfileForm">
<!-- name-input -->
<div class="col-sm-12 form-section-title">Device Profile Details </div>
<div class="form-group">
<label for="stack_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input
type="text"
class="form-control"
ng-model="formValues.name"
id="profile_name"
name="profile_name"
placeholder="e.g. myprofile"
auto-focus
data-cy="profile-name-input"
/>
</div>
</div>
<!-- !name-input -->
<!-- build-method -->
<div class="col-sm-12 form-section-title"> Profile configuration </div>
<box-selector slim="true" options="buildMethods" value="state.method"></box-selector>
<!-- !build-method -->
<web-editor-form
ng-if="state.method === 'editor'"
identifier="profile-creation-editor"
value="formValues.profileFileContent"
on-change="(onChangeFileContent)"
ng-required="true"
>
</web-editor-form>
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<a type="button" class="btn btn-default btn-sm" ui-sref="portainer.settings.edgeCompute">Cancel</a>
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress
|| !createProfileForm.$valid
|| !formValues.profileFileContent
|| !formValues.name"
ng-click="createProfileAsync()"
button-spinner="state.actionInProgress"
>
<span ng-hide="state.actionInProgress">Save Profile</span>
<span ng-show="state.actionInProgress">Saving...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -1,70 +0,0 @@
import angular from 'angular';
import { editor } from '@@/BoxSelector/common-options/build-methods';
import { createProfile } from 'Portainer/hostmanagement/fdo/fdo.service';
angular.module('portainer.app').controller('AddProfileController', AddProfileController);
/* @ngInject */
export default function AddProfileController($scope, $async, $state, $window, Notifications) {
$scope.buildMethods = [editor];
$scope.formValues = {
name: '',
profileFileContent: '',
};
$scope.state = {
method: 'editor',
actionInProgress: false,
isEditorDirty: false,
};
$window.onbeforeunload = () => {
if ($scope.state.method === 'editor' && $scope.formValues.profileFileContent && $scope.state.isEditorDirty) {
return '';
}
};
$scope.$on('$destroy', function () {
$scope.state.isEditorDirty = false;
});
$scope.onChangeFormValues = onChangeFormValues;
$scope.createProfileAsync = function () {
return $async(async () => {
const method = $scope.state.method;
const name = $scope.formValues.name;
const fileContent = $scope.formValues.profileFileContent;
if (method !== 'editor' && fileContent === '') {
$scope.state.formValidationError = 'Profile file content must not be empty';
return;
}
$scope.state.actionInProgress = true;
try {
await createProfile(name, method, fileContent);
Notifications.success('Success', 'Profile successfully created');
$scope.state.isEditorDirty = false;
$state.go('portainer.settings.edgeCompute');
} catch (err) {
Notifications.error('Failure', err, 'Unable to create Profile');
} finally {
$scope.state.actionInProgress = false;
}
});
};
$scope.onChangeFileContent = function onChangeFileContent(value) {
$scope.formValues.profileFileContent = value;
$scope.state.isEditorDirty = true;
};
function onChangeFormValues(newValues) {
$scope.formValues = newValues;
}
}

View file

@ -1,8 +0,0 @@
import angular from 'angular';
import controller from './addProfileController';
angular.module('portainer.app').component('addProfileView', {
templateUrl: './addProfile.html',
controller,
});

View file

@ -1,66 +0,0 @@
<page-header title="'Edit profile'" breadcrumbs="[{label:'Settings', link:'portainer.settings'}, 'Edge Compute']" reload="true"> </page-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="editProfileForm">
<!-- name-input -->
<div class="col-sm-12 form-section-title">Device Profile Details </div>
<div class="form-group">
<label for="stack_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input
type="text"
class="form-control"
ng-model="formValues.name"
id="profile_name"
name="profile_name"
placeholder="e.g. myprofile"
auto-focus
data-cy="profile-name-input"
/>
</div>
</div>
<!-- !name-input -->
<!-- build-method -->
<div class="col-sm-12 form-section-title"> Profile configuration </div>
<box-selector slim="true" options="buildMethods" value="state.method"></box-selector>
<!-- !build-method -->
<web-editor-form
ng-if="state.method === 'editor'"
identifier="profile-creation-editor"
value="formValues.profileFileContent"
on-change="(onChangeFileContent)"
ng-required="true"
>
</web-editor-form>
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<a type="button" class="btn btn-default btn-sm" ui-sref="portainer.settings.edgeCompute">Cancel</a>
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress
|| !editProfileForm.$valid
|| !formValues.profileFileContent
|| !formValues.name"
ng-click="updateProfileAsync()"
button-spinner="state.actionInProgress"
>
<span ng-hide="state.actionInProgress">Update Profile</span>
<span ng-show="state.actionInProgress">Saving...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -1,88 +0,0 @@
import angular from 'angular';
import { editor } from '@@/BoxSelector/common-options/build-methods';
import { getProfile, updateProfile } from 'Portainer/hostmanagement/fdo/fdo.service';
angular.module('portainer.app').controller('EditProfileController', EditProfileController);
/* @ngInject */
export default function EditProfileController($scope, $async, $state, $window, Notifications) {
$scope.buildMethods = [editor];
$scope.formValues = {
name: '',
profileFileContent: '',
};
$scope.state = {
profileID: $state.params.id,
method: 'editor',
actionInProgress: false,
isEditorDirty: false,
};
$window.onbeforeunload = () => {
if ($scope.state.method === 'editor' && $scope.formValues.profileFileContent && $scope.state.isEditorDirty) {
return '';
}
};
$scope.$on('$destroy', function () {
$scope.state.isEditorDirty = false;
});
$scope.onChangeFormValues = onChangeFormValues;
$scope.updateProfileAsync = function () {
return $async(async () => {
const method = $scope.state.method;
const name = $scope.formValues.name;
const fileContent = $scope.formValues.profileFileContent;
if (method !== 'editor' && fileContent === '') {
$scope.state.formValidationError = 'Profile file content must not be empty';
return;
}
$scope.state.actionInProgress = true;
try {
await updateProfile($scope.state.profileID, name, fileContent);
Notifications.success('Success', 'Profile successfully updated');
$scope.state.isEditorDirty = false;
$state.go('portainer.settings.edgeCompute');
} catch (err) {
Notifications.error('Failure', err, 'Unable to update Profile');
} finally {
$scope.state.actionInProgress = false;
}
});
};
$scope.onChangeFileContent = function onChangeFileContent(value) {
$scope.formValues.profileFileContent = value;
$scope.state.isEditorDirty = true;
};
function onChangeFormValues(newValues) {
$scope.formValues = newValues;
}
async function initView() {
return $async(async () => {
try {
const profile = await getProfile($scope.state.profileID);
$scope.formValues = {
name: profile.name,
profileFileContent: profile.fileContent,
};
$scope.state.isEditorDirty = false;
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve profile details');
}
});
}
initView();
}

View file

@ -1,8 +0,0 @@
import angular from 'angular';
import controller from './editProfileController';
angular.module('portainer.app').component('editProfileView', {
templateUrl: './editProfile.html',
controller,
});

View file

@ -11,9 +11,3 @@
<settings-open-amt on-submit="($ctrl.onSubmitOpenAMT)" settings="($ctrl.settings)"></settings-open-amt>
</div>
</div>
<div class="row">
<div class="col-sm-12" ng-if="$ctrl.settings">
<settings-fdo on-submit="($ctrl.onSubmitFDO)" settings="($ctrl.settings)"></settings-fdo>
</div>
</div>

View file

@ -1,7 +1,6 @@
import _ from 'lodash-es';
import angular from 'angular';
import { configureFDO } from '@/portainer/hostmanagement/fdo/fdo.service';
import { configureAMT } from 'Portainer/hostmanagement/open-amt/open-amt.service';
angular.module('portainer.app').controller('SettingsEdgeComputeController', SettingsEdgeComputeController);
@ -31,16 +30,6 @@ export default function SettingsEdgeComputeController($q, $async, $state, Notifi
}
};
this.onSubmitFDO = async function (formValues) {
try {
await configureFDO(formValues);
Notifications.success('Success', `FDO successfully ${formValues.enabled ? 'enabled' : 'disabled'}`);
$state.reload();
} catch (err) {
Notifications.error('Failure', err, 'Failed applying changes');
}
};
function initView() {
$async(async () => {
try {

View file

@ -15,7 +15,6 @@ import { EnvironmentStatus } from '../types';
import { columns } from './columns';
import { EnvironmentListItem } from './types';
import { ImportFdoDeviceButton } from './ImportFdoDeviceButton';
const tableKey = 'environments';
const settingsStore = createPersistedStore(tableKey, 'Name');
@ -83,8 +82,6 @@ export function EnvironmentsDatatable({
Remove
</Button>
<ImportFdoDeviceButton />
{isBE && (
<AddButton
color="secondary"

View file

@ -1,32 +0,0 @@
import { AddButton } from '@@/buttons';
import { useSettings } from '../../settings/queries';
import {
FeatureFlag,
useFeatureFlag,
} from '../../feature-flags/useFeatureFlag';
export function ImportFdoDeviceButton() {
const flagEnabledQuery = useFeatureFlag(FeatureFlag.FDO);
const isFDOEnabledQuery = useSettings(
(settings) => settings.fdoConfiguration.enabled,
flagEnabledQuery.data
);
if (!isFDOEnabledQuery.data || !flagEnabledQuery.data) {
return null;
}
return (
<div className="ml-[5px]">
<AddButton
color="secondary"
to="portainer.endpoints.importDevice"
data-cy="import-fdo-device-button"
>
Import FDO device
</AddButton>
</div>
);
}

View file

@ -1,8 +1,6 @@
import { usePublicSettings } from '../settings/queries';
export enum FeatureFlag {
FDO = 'fdo',
}
export enum FeatureFlag {}
export function useFeatureFlag(
flag: FeatureFlag,

View file

@ -1,45 +0,0 @@
import { List } from 'lucide-react';
import { Datatable } from '@@/datatables';
import { createPersistedStore } from '@@/datatables/types';
import { useTableState } from '@@/datatables/useTableState';
import { columns } from './columns';
import { FDOProfilesDatatableActions } from './FDOProfilesDatatableActions';
import { useFDOProfiles } from './useFDOProfiles';
const storageKey = 'fdoProfiles';
const settingsStore = createPersistedStore(storageKey, 'name');
export interface FDOProfilesDatatableProps {
isFDOEnabled: boolean;
}
export function FDOProfilesDatatable({
isFDOEnabled,
}: FDOProfilesDatatableProps) {
const tableState = useTableState(settingsStore, storageKey);
const { isLoading, profiles } = useFDOProfiles();
return (
<Datatable
columns={columns}
dataset={profiles}
settingsManager={tableState}
title="Device Profiles"
titleIcon={List}
disableSelect={!isFDOEnabled}
getRowId={(row) => row.id.toString()}
isLoading={isLoading}
renderTableActions={(selectedItems) => (
<FDOProfilesDatatableActions
isFDOEnabled={isFDOEnabled}
selectedItems={selectedItems}
/>
)}
data-cy="fdo-profiles-datatable"
/>
);
}

View file

@ -1,108 +0,0 @@
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from '@uirouter/react';
import { PlusCircle } from 'lucide-react';
import { Profile } from '@/portainer/hostmanagement/fdo/model';
import * as notifications from '@/portainer/services/notifications';
import {
deleteProfile,
duplicateProfile,
} from '@/portainer/hostmanagement/fdo/fdo.service';
import { confirm } from '@@/modals/confirm';
import { Link } from '@@/Link';
import { Button } from '@@/buttons';
import { DeleteButton } from '@@/buttons/DeleteButton';
interface Props {
isFDOEnabled: boolean;
selectedItems: Profile[];
}
export function FDOProfilesDatatableActions({
isFDOEnabled,
selectedItems,
}: Props) {
const router = useRouter();
const queryClient = useQueryClient();
return (
<>
<Link
to="portainer.endpoints.profile"
className="space-left"
data-cy="fdo-add-profile-link"
>
<Button
disabled={!isFDOEnabled}
icon={PlusCircle}
data-cy="fdo-add-profile-button"
>
Add Profile
</Button>
</Link>
<Button
disabled={!isFDOEnabled || selectedItems.length !== 1}
data-cy="fdo-duplicate-profile-button"
onClick={() => onDuplicateProfileClick()}
icon={PlusCircle}
>
Duplicate
</Button>
<DeleteButton
disabled={!isFDOEnabled || selectedItems.length === 0}
onConfirmed={() => onDeleteProfileClick()}
confirmMessage="This action will delete the selected profile(s). Continue?"
data-cy="fdo-remove-profile-button"
/>
</>
);
async function onDuplicateProfileClick() {
const confirmed = await confirm({
title: 'Are you sure ?',
message: 'This action will duplicate the selected profile. Continue?',
});
if (!confirmed) {
return;
}
try {
const profile = selectedItems[0];
const newProfile = await duplicateProfile(profile.id);
notifications.success('Profile successfully duplicated', profile.name);
router.stateService.go('portainer.endpoints.profile.edit', {
id: newProfile.id,
});
} catch (err) {
notifications.error(
'Failure',
err as Error,
'Unable to duplicate profile'
);
}
}
async function onDeleteProfileClick() {
await Promise.all(
selectedItems.map(async (profile) => {
try {
await deleteProfile(profile.id);
notifications.success('Profile successfully removed', profile.name);
} catch (err) {
notifications.error(
'Failure',
err as Error,
'Unable to remove profile'
);
}
})
);
await queryClient.invalidateQueries(['fdo_profiles']);
}
}

View file

@ -1,12 +0,0 @@
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
import { columnHelper } from './helper';
export const created = columnHelper.accessor('dateCreated', {
header: 'Created',
id: 'created',
cell: ({ getValue }) => {
const value = getValue();
return isoDateFromTimestamp(value);
},
});

View file

@ -1,5 +0,0 @@
import { createColumnHelper } from '@tanstack/react-table';
import { Profile } from '@/portainer/hostmanagement/fdo/model';
export const columnHelper = createColumnHelper<Profile>();

View file

@ -1,14 +0,0 @@
import { Profile } from '@/portainer/hostmanagement/fdo/model';
import { buildNameColumn } from '@@/datatables/buildNameColumn';
import { created } from './created';
export const columns = [
buildNameColumn<Profile>(
'name',
'portainer.endpoints.profile.edit',
'fdo-profiles-name'
),
created,
];

View file

@ -1 +0,0 @@
export { FDOProfilesDatatable } from './FDOProfilesDatatable';

View file

@ -1,30 +0,0 @@
import { useEffect, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import PortainerError from '@/portainer/error';
import * as notifications from '@/portainer/services/notifications';
import { getProfiles } from '@/portainer/hostmanagement/fdo/fdo.service';
export function useFDOProfiles() {
const { isLoading, data, isError, error } = useQuery(['fdo_profiles'], () =>
getProfiles()
);
useEffect(() => {
if (isError) {
notifications.error(
'Failure',
error as Error,
'Failed retrieving FDO profiles'
);
}
}, [isError, error]);
const profiles = useMemo(() => data || [], [data]);
return {
isLoading,
profiles,
error: isError ? (error as PortainerError) : undefined,
};
}

View file

@ -1,3 +0,0 @@
.fdo-table {
margin-top: 3em;
}

View file

@ -1,212 +0,0 @@
import { useEffect, useState } from 'react';
import { Formik, Field, Form } from 'formik';
import { FlaskConical, Laptop } from 'lucide-react';
import { FDOConfiguration } from '@/portainer/hostmanagement/fdo/model';
import {
FeatureFlag,
useFeatureFlag,
} from '@/react/portainer/feature-flags/useFeatureFlag';
import { Switch } from '@@/form-components/SwitchField/Switch';
import { FormControl } from '@@/form-components/FormControl';
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
import { LoadingButton } from '@@/buttons/LoadingButton';
import { TextTip } from '@@/Tip/TextTip';
import { Input } from '@@/form-components/Input';
import { FDOProfilesDatatable } from '../FDOProfilesDatatable';
import styles from './SettingsFDO.module.css';
import { validationSchema } from './SettingsFDO.validation';
export interface Settings {
fdoConfiguration: FDOConfiguration;
EnableEdgeComputeFeatures: boolean;
}
interface Props {
settings: Settings;
onSubmit(values: FDOConfiguration): void;
}
export function SettingsFDO({ settings, onSubmit }: Props) {
const flagEnabledQuery = useFeatureFlag(FeatureFlag.FDO);
if (!flagEnabledQuery.data) {
return (
<Widget>
<Widget.Body>
<TextTip color="blue" icon={FlaskConical}>
Since FDO is still an experimental feature that requires additional
infrastructure, it has been temporarily hidden in the UI.
</TextTip>
</Widget.Body>
</Widget>
);
}
return <SettingsFDOForm settings={settings} onSubmit={onSubmit} />;
}
export function SettingsFDOForm({ settings, onSubmit }: Props) {
const fdoConfiguration = settings ? settings.fdoConfiguration : null;
const initialFDOEnabled = fdoConfiguration ? fdoConfiguration.enabled : false;
const [isFDOEnabled, setIsFDOEnabled] = useState(initialFDOEnabled);
useEffect(() => {
setIsFDOEnabled(settings?.fdoConfiguration?.enabled);
}, [settings]);
const initialValues = {
enabled: initialFDOEnabled,
ownerURL: fdoConfiguration ? fdoConfiguration.ownerURL : '',
ownerUsername: fdoConfiguration ? fdoConfiguration.ownerUsername : '',
ownerPassword: fdoConfiguration ? fdoConfiguration.ownerPassword : '',
};
const edgeComputeFeaturesEnabled = settings
? settings.EnableEdgeComputeFeatures
: false;
return (
<div className="row">
<Widget>
<WidgetTitle icon={Laptop} title="FDO" />
<WidgetBody>
<Formik
initialValues={initialValues}
onSubmit={onSubmit}
enableReinitialize
validationSchema={() => validationSchema()}
validateOnChange
validateOnMount
>
{({
values,
errors,
handleSubmit,
setFieldValue,
isSubmitting,
isValid,
dirty,
}) => (
<Form className="form-horizontal" onSubmit={handleSubmit}>
<FormControl
inputId="edge_enableFDO"
label="Enable FDO Management Service"
size="small"
errors={errors.enabled}
>
<Switch
id="edge_enableFDO"
data-cy="edge-enableFDO-switch"
name="edge_enableFDO"
className="space-right"
disabled={!edgeComputeFeaturesEnabled}
checked={edgeComputeFeaturesEnabled && values.enabled}
onChange={(e) => onChangedEnabled(e, setFieldValue)}
/>
</FormControl>
<TextTip color="blue" className="mb-2">
When enabled, this will allow Portainer to interact with FDO
Services.
</TextTip>
{edgeComputeFeaturesEnabled && values.enabled && (
<>
<hr />
<FormControl
inputId="owner_url"
label="Owner Service Server"
errors={errors.ownerURL}
>
<Field
as={Input}
name="ownerURL"
id="owner_url"
placeholder="http://127.0.0.1:8042"
value={values.ownerURL}
data-cy="fdo-serverInput"
/>
</FormControl>
<FormControl
inputId="owner_username"
label="Owner Service Username"
errors={errors.ownerUsername}
>
<Field
as={Input}
name="ownerUsername"
id="owner_username"
placeholder="username"
value={values.ownerUsername}
data-cy="fdo-usernameInput"
/>
</FormControl>
<FormControl
inputId="owner_password"
label="Owner Service Password"
errors={errors.ownerPassword}
>
<Field
as={Input}
type="password"
name="ownerPassword"
id="owner_password"
placeholder="password"
value={values.ownerPassword}
data-cy="fdo-passwordInput"
/>
</FormControl>
</>
)}
<div className="form-group mt-5">
<div className="col-sm-12">
<LoadingButton
disabled={!isValid || !dirty}
data-cy="settings-fdoButton"
isLoading={isSubmitting}
loadingText="Saving settings..."
>
Save settings
</LoadingButton>
</div>
</div>
</Form>
)}
</Formik>
{edgeComputeFeaturesEnabled && isFDOEnabled && (
<div className={styles.fdoTable}>
<FormSectionTitle>Device Profiles</FormSectionTitle>
<TextTip color="blue" className="mb-2">
Add, Edit and Manage the list of device profiles available
during FDO device setup
</TextTip>
<FDOProfilesDatatable isFDOEnabled={initialFDOEnabled} />
</div>
)}
</WidgetBody>
</Widget>
</div>
);
async function onChangedEnabled(
e: boolean,
setFieldValue: (
field: string,
value: unknown,
shouldValidate?: boolean
) => void
) {
setIsFDOEnabled(e);
setFieldValue('enabled', e);
}
}

View file

@ -1,18 +0,0 @@
import { object, string } from 'yup';
export function validationSchema() {
return object().shape({
ownerURL: string().when('enabled', {
is: true,
then: string().required('Field is required'),
}),
ownerUsername: string().when('enabled', {
is: true,
then: string().required('Field is required'),
}),
ownerPassword: string().when('enabled', {
is: true,
then: string().required('Field is required'),
}),
});
}

View file

@ -1 +0,0 @@
export { SettingsFDO } from './SettingsFDO';

View file

@ -253,7 +253,7 @@ export function SettingsOpenAMT({ settings, onSubmit }: Props) {
<div className="col-sm-12">
<LoadingButton
disabled={!isValid || !dirty}
data-cy="settings-fdoButton"
data-cy="settings-OpenAMTButton"
isLoading={isSubmitting}
loadingText="Saving settings..."
>

View file

@ -1,12 +1,5 @@
import { TeamId } from '@/react/portainer/users/teams/types';
export interface FDOConfiguration {
enabled: boolean;
ownerURL: string;
ownerUsername: string;
ownerPassword: string;
}
export interface TLSConfiguration {
TLS: boolean;
TLSSkipVerify: boolean;
@ -119,7 +112,6 @@ export interface Settings {
LDAPSettings: LDAPSettings;
OAuthSettings: OAuthSettings;
openAMTConfiguration: OpenAMTConfiguration;
fdoConfiguration: FDOConfiguration;
FeatureFlagSettings: { [key: Feature]: boolean };
SnapshotInterval: string;
TemplatesURL: string;
@ -201,8 +193,6 @@ export interface PublicSettingsResponse {
KubeconfigExpiry: string;
/** Whether team sync is enabled */
TeamSync: boolean;
/** Whether FDO is enabled */
IsFDOEnabled: boolean;
/** Whether AMT is enabled */
IsAMTEnabled: boolean;
/** Whether to hide default registry (only on BE) */