mirror of
https://github.com/portainer/portainer.git
synced 2025-08-09 15:55:23 +02:00
feat(intel): Enable OpenAMT and FDO capabilities (#6212)
* feat(openamt): add AMT Devices information in Environments view [INT-8] (#6169) * feat(openamt): add AMT Devices Ouf of Band Managamenet actions [INT-9] (#6171) * feat(openamt): add AMT Devices KVM Connection [INT-10] (#6179) * feat(openamt): Enhance the Environments MX to activate OpenAMT on compatible environments [INT-7] (#6196) * feat(openamt): Enable KVM by default [INT-25] (#6228) * feat(fdo): implement the FDO configuration settings INT-19 (#6238) feat(fdo): implement the FDO configuration settings INT-19 * feat(fdo): implement Owner client INT-17 (#6231) feat(fdo): implement Owner client INT-17 * feat(openamt): hide wireless config in OpenAMT form (#6250) * feat(openamt): Increase OpenAMT timeouts [INT-30] (#6253) * feat(openamt): Disable the ability to use KVM and OOB actions on a MPS disconnected device [INT-36] (#6254) * feat(fdo): add import device UI [INT-20] (#6240) feat(fdo): add import device UI INT-20 * refactor(fdo): fix develop merge issues * feat(openamt): Do not fetch OpenAMT details for an unassociated Edge endpoint (#6273) * fix(intel): Fix switches params (#6282) * feat(openamt): preload existing AMT settings (#6283) * feat(openamt): Better UI/UX for AMT activation loading [INT-39] (#6290) * feat(openamt): Remove wireless config related code [INT-41] (#6291) * yarn install * feat(openamt): change kvm redirection for pop up, always enable features [INT-37] (#6292) * feat(openamt): change kvm redirection for pop up, always enable features [INT-37] (#6293) * feat(openmt): use .ts services with axios for OpenAMT (#6312) * Minor code cleanup. * fix(fdo): move the FDO client code to the hostmanagement folder INT-44 (#6345) * refactor(intel): Add Edge Compute Settings view (#6351) * feat(fdo): add FDO profiles INT-22 (#6363) feat(fdo): add FDO profiles INT-22 * fix(fdo): fix incorrect profile URL INT-45 (#6377) * fixed husky version * fix go.mod with go mod tidy * feat(edge): migrate OpenAMT devices views to Edge Devices [EE-2322] (#6373) * feat(intel): OpenAMT UI/UX adjustments (#6394) * only allow edge agent as edge device * show all edge agent environments on Edge Devices view * feat(fdo): add the ability to import multiple ownership vouchers at once EE-2324 (#6395) * fix(edge): settings edge compute alert (#6402) * remove pagination, add useMemo for devices result array (#6409) * feat(edge): minor Edge Devices (AMT) UI fixes (#6410) * chore(eslint): fix versions * chore(app): reformat codebase * change add edge agent modal behaviour, fix yarn.lock * fix use pagination * remove extractedTranslations folder * feat(edge): add FDO Profiles Datatable [EE-2406] (#6415) * feat(edge): add KVM workaround tooltip (#6441) * feat(edge): Add default FDO profile (#6450) * feat(edge): add settings to disable trust on first connect and enforce Edge ID INT-1 EE-2410 (#6429) Co-authored-by: andres-portainer <91705312+andres-portainer@users.noreply.github.com> Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io> Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com> Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
This commit is contained in:
parent
3ed92e5fee
commit
2c4c638f46
170 changed files with 6834 additions and 819 deletions
|
@ -205,6 +205,51 @@ angular
|
|||
},
|
||||
};
|
||||
|
||||
var edgeDeviceCreation = {
|
||||
name: 'portainer.endpoints.newEdgeDevice',
|
||||
url: '/newEdgeDevice',
|
||||
params: {
|
||||
isEdgeDevice: true,
|
||||
},
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/endpoints/create/createendpoint.html',
|
||||
controller: 'CreateEndpointController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var deviceImport = {
|
||||
name: 'portainer.endpoints.importDevice',
|
||||
url: '/device',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/devices/import/importDevice.html',
|
||||
controller: 'ImportDeviceController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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',
|
||||
|
@ -217,6 +262,17 @@ angular
|
|||
},
|
||||
};
|
||||
|
||||
var endpointKVM = {
|
||||
name: 'portainer.endpoints.endpoint.kvm',
|
||||
url: '/kvm?deviceId&deviceName',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/endpoints/kvm/endpointKVM.html',
|
||||
controller: 'EndpointKVMController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var groups = {
|
||||
name: 'portainer.groups',
|
||||
url: '/groups',
|
||||
|
@ -378,6 +434,16 @@ angular
|
|||
},
|
||||
};
|
||||
|
||||
var settingsEdgeCompute = {
|
||||
name: 'portainer.settings.edgeCompute',
|
||||
url: '/edge',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'settingsEdgeComputeView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var tags = {
|
||||
name: 'portainer.tags',
|
||||
url: '/tags',
|
||||
|
@ -444,7 +510,12 @@ angular
|
|||
$stateRegistryProvider.register(endpoint);
|
||||
$stateRegistryProvider.register(k8sendpoint);
|
||||
$stateRegistryProvider.register(endpointAccess);
|
||||
$stateRegistryProvider.register(endpointKVM);
|
||||
$stateRegistryProvider.register(edgeDeviceCreation);
|
||||
$stateRegistryProvider.register(endpointCreation);
|
||||
$stateRegistryProvider.register(deviceImport);
|
||||
$stateRegistryProvider.register(addFDOProfile);
|
||||
$stateRegistryProvider.register(editFDOProfile);
|
||||
$stateRegistryProvider.register(endpointKubernetesConfiguration);
|
||||
$stateRegistryProvider.register(groups);
|
||||
$stateRegistryProvider.register(group);
|
||||
|
@ -461,6 +532,7 @@ angular
|
|||
$stateRegistryProvider.register(registryCreation);
|
||||
$stateRegistryProvider.register(settings);
|
||||
$stateRegistryProvider.register(settingsAuthentication);
|
||||
$stateRegistryProvider.register(settingsEdgeCompute);
|
||||
$stateRegistryProvider.register(tags);
|
||||
$stateRegistryProvider.register(users);
|
||||
$stateRegistryProvider.register(user);
|
||||
|
|
|
@ -3,14 +3,23 @@ import clsx from 'clsx';
|
|||
|
||||
import styles from './TextTip.module.css';
|
||||
|
||||
export function TextTip({ children }: PropsWithChildren<unknown>) {
|
||||
type Color = 'orange' | 'blue';
|
||||
|
||||
export interface Props {
|
||||
color?: Color;
|
||||
}
|
||||
|
||||
export function TextTip({
|
||||
color = 'orange',
|
||||
children,
|
||||
}: PropsWithChildren<Props>) {
|
||||
return (
|
||||
<p className="text-muted small">
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className={clsx(
|
||||
'fa fa-exclamation-circle',
|
||||
'orange-icon',
|
||||
`${color}-icon`,
|
||||
styles.textMargin
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
.actions {
|
||||
float: right;
|
||||
margin: 5px 10px 0 0;
|
||||
}
|
||||
|
||||
.actions-active {
|
||||
color: var(--blue-2);
|
||||
}
|
||||
|
||||
.table-actions-menu-list {
|
||||
padding: 0 10px 0 10px;
|
||||
}
|
||||
|
||||
.table-actions-menu-list [data-reach-menu-item] {
|
||||
padding: 5px 5px !important;
|
||||
}
|
||||
|
||||
.table-actions-menu-btn {
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
[data-reach-menu-link] {
|
||||
text-decoration: none !important;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import { ReactNode } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Menu, MenuList, MenuButton } from '@reach/menu-button';
|
||||
|
||||
import styles from './ActionsMenu.module.css';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ActionsMenu({ children }: Props) {
|
||||
return (
|
||||
<Menu className={styles.actions}>
|
||||
{({ isExpanded }) => (
|
||||
<>
|
||||
<MenuButton
|
||||
className={clsx(
|
||||
styles.tableActionsMenuBtn,
|
||||
isExpanded && styles.actionsActive
|
||||
)}
|
||||
>
|
||||
<i className="fa fa-ellipsis-v" aria-hidden="true" />
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<div className={styles.tableActionsMenuList}>{children}</div>
|
||||
</MenuList>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.table-actions-title {
|
||||
color: var(--blue-2);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { ReactNode } from 'react';
|
||||
|
||||
import styles from './ActionsMenuTitle.module.css';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ActionsMenuTitle({ children }: Props) {
|
||||
return <div className={styles.tableActionsTitle}>{children}</div>;
|
||||
}
|
|
@ -3,17 +3,20 @@ import { Menu, MenuButton, MenuList } from '@reach/menu-button';
|
|||
import { ColumnInstance } from 'react-table';
|
||||
|
||||
import { Checkbox } from '@/portainer/components/form-components/Checkbox';
|
||||
import type { DockerContainer } from '@/docker/containers/types';
|
||||
|
||||
import { useTableContext } from './TableContainer';
|
||||
|
||||
interface Props {
|
||||
columns: ColumnInstance<DockerContainer>[];
|
||||
interface Props<D extends object> {
|
||||
columns: ColumnInstance<D>[];
|
||||
onChange: (value: string[]) => void;
|
||||
value: string[];
|
||||
}
|
||||
|
||||
export function ColumnVisibilityMenu({ columns, onChange, value }: Props) {
|
||||
export function ColumnVisibilityMenu<D extends object>({
|
||||
columns,
|
||||
onChange,
|
||||
value,
|
||||
}: Props<D>) {
|
||||
useTableContext();
|
||||
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import { PropsWithChildren } from 'react';
|
||||
import { Row, TableInstance } from 'react-table';
|
||||
|
||||
interface Props {
|
||||
row: Row<TableInstance>;
|
||||
}
|
||||
|
||||
export function ExpandingCell({ children, row }: PropsWithChildren<Props>) {
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<div {...row.getToggleRowExpandedProps()}>
|
||||
<i
|
||||
className={`fas ${arrowClass(row.isExpanded)} space-right`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
function arrowClass(isExpanded: boolean) {
|
||||
if (isExpanded) {
|
||||
return 'fa-angle-down';
|
||||
}
|
||||
return 'fa-angle-right';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.inner-datatable thead {
|
||||
background-color: var(--bg-inner-datatable-thead) !important;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import styles from './InnerDatatable.module.css';
|
||||
|
||||
export function InnerDatatable({ children }: PropsWithChildren<unknown>) {
|
||||
return <div className={styles.innerDatatable}>{children}</div>;
|
||||
}
|
|
@ -5,7 +5,7 @@ import { TableHeaderCell } from './TableHeaderCell';
|
|||
|
||||
interface Props<D extends Record<string, unknown> = Record<string, unknown>> {
|
||||
headers: HeaderGroup<D>[];
|
||||
onSortChange(colId: string, desc: boolean): void;
|
||||
onSortChange?(colId: string, desc: boolean): void;
|
||||
}
|
||||
|
||||
export function TableHeaderRow<
|
||||
|
@ -26,13 +26,18 @@ export function TableHeaderRow<
|
|||
headerProps={{
|
||||
...column.getHeaderProps({
|
||||
className: column.className,
|
||||
style: {
|
||||
width: column.disableResizing ? column.width : '',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
key={column.id}
|
||||
canSort={column.canSort}
|
||||
onSortClick={(desc) => {
|
||||
column.toggleSortBy(desc);
|
||||
onSortChange(column.id, desc);
|
||||
if (onSortChange) {
|
||||
onSortChange(column.id, desc);
|
||||
}
|
||||
}}
|
||||
isSorted={column.isSorted}
|
||||
isSortedDesc={column.isSortedDesc}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { PropsWithChildren, ReactNode } from 'react';
|
|||
import { useTableContext } from './TableContainer';
|
||||
|
||||
interface Props {
|
||||
quickActions: ReactNode;
|
||||
quickActions?: ReactNode;
|
||||
}
|
||||
|
||||
export function TableSettingsMenu({
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.endpoints.new" data-cy="endpoint-addEndpointButton">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add environment
|
||||
<i class="fa fa-plus-circle space-right" aria-hidden="true"></i>Add environment
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
|
|
|
@ -3,6 +3,7 @@ angular.module('portainer.app').controller('EndpointsDatatableController', [
|
|||
'$controller',
|
||||
'DatatableService',
|
||||
'PaginationService',
|
||||
'Notifications',
|
||||
function ($scope, $controller, DatatableService, PaginationService) {
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
.file-input {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.file-button {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import styles from './FileUploadField.module.css';
|
|||
export interface Props {
|
||||
onChange(value: File): void;
|
||||
value?: File;
|
||||
accept?: string;
|
||||
title?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
@ -15,6 +16,7 @@ export interface Props {
|
|||
export function FileUploadField({
|
||||
onChange,
|
||||
value,
|
||||
accept,
|
||||
title = 'Select a file',
|
||||
required = false,
|
||||
}: Props) {
|
||||
|
@ -25,12 +27,18 @@ export function FileUploadField({
|
|||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept={accept}
|
||||
required={required}
|
||||
className={styles.fileInput}
|
||||
onChange={changeHandler}
|
||||
aria-label="file-input"
|
||||
/>
|
||||
<Button size="small" color="primary" onClick={handleButtonClick}>
|
||||
<Button
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={handleButtonClick}
|
||||
className={styles.fileButton}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
|
||||
|
|
|
@ -5,9 +5,12 @@ import { Tooltip } from '@/portainer/components/Tip/Tooltip';
|
|||
|
||||
import styles from './FormControl.module.css';
|
||||
|
||||
type Size = 'small' | 'medium' | 'large';
|
||||
|
||||
export interface Props {
|
||||
inputId: string;
|
||||
label: string | ReactNode;
|
||||
size?: Size;
|
||||
tooltip?: string;
|
||||
children: ReactNode;
|
||||
errors?: string | ReactNode;
|
||||
|
@ -16,6 +19,7 @@ export interface Props {
|
|||
export function FormControl({
|
||||
inputId,
|
||||
label,
|
||||
size = 'small',
|
||||
tooltip = '',
|
||||
children,
|
||||
errors,
|
||||
|
@ -25,13 +29,13 @@ export function FormControl({
|
|||
<div className={clsx('form-group', styles.container)}>
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="col-sm-3 col-lg-2 control-label text-left"
|
||||
className={`${sizeClassLabel(size)} control-label text-left`}
|
||||
>
|
||||
{label}
|
||||
{tooltip && <Tooltip message={tooltip} />}
|
||||
</label>
|
||||
|
||||
<div className="col-sm-9 col-lg-10">{children}</div>
|
||||
<div className={`${sizeClassChildren(size)}`}>{children}</div>
|
||||
</div>
|
||||
|
||||
{errors && (
|
||||
|
@ -48,3 +52,25 @@ export function FormControl({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function sizeClassLabel(size?: Size) {
|
||||
switch (size) {
|
||||
case 'large':
|
||||
return 'col-sm-5 col-lg-4';
|
||||
case 'medium':
|
||||
return 'col-sm-4 col-lg-3';
|
||||
default:
|
||||
return 'col-sm-3 col-lg-2';
|
||||
}
|
||||
}
|
||||
|
||||
function sizeClassChildren(size?: Size) {
|
||||
switch (size) {
|
||||
case 'large':
|
||||
return 'col-sm-7 col-lg-8';
|
||||
case 'medium':
|
||||
return 'col-sm-8 col-lg-9';
|
||||
default:
|
||||
return 'col-sm-9 col-lg-10';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,6 +94,7 @@ interface EndpointOptions {
|
|||
checkinInterval?: number;
|
||||
azure?: AzureSettings;
|
||||
tls?: TLSSettings;
|
||||
isEdgeDevice?: boolean;
|
||||
}
|
||||
|
||||
export async function createRemoteEndpoint(
|
||||
|
@ -134,6 +135,7 @@ async function createEndpoint(
|
|||
GroupID: options.groupId,
|
||||
TagIds: arrayToJson(options.tagIds),
|
||||
CheckinInterval: options.checkinInterval,
|
||||
IsEdgeDevice: options.isEdgeDevice,
|
||||
};
|
||||
|
||||
const { tls, azure } = options;
|
||||
|
|
|
@ -20,6 +20,7 @@ interface EndpointsQuery {
|
|||
endpointIds?: EnvironmentId[];
|
||||
tagsPartialMatch?: boolean;
|
||||
groupId?: EnvironmentGroupId;
|
||||
edgeDeviceFilter?: boolean;
|
||||
}
|
||||
|
||||
export async function getEndpoints(
|
||||
|
@ -222,3 +223,14 @@ export async function updateSettings(
|
|||
throw parseAxiosError(e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function trustEndpoint(id: EnvironmentId) {
|
||||
try {
|
||||
const { data: endpoint } = await axios.put<Environment>(buildUrl(id), {
|
||||
UserTrusted: true,
|
||||
});
|
||||
return endpoint;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to update environment');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ export interface KubernetesSettings {
|
|||
Snapshots: KubernetesSnapshot[];
|
||||
}
|
||||
|
||||
export interface Environment {
|
||||
export type Environment = {
|
||||
Id: EnvironmentId;
|
||||
Type: EnvironmentType;
|
||||
TagIds: TagId[];
|
||||
|
@ -72,7 +72,10 @@ export interface Environment {
|
|||
Snapshots: DockerSnapshot[];
|
||||
Kubernetes: KubernetesSettings;
|
||||
PublicURL?: string;
|
||||
}
|
||||
IsEdgeDevice?: boolean;
|
||||
UserTrusted: boolean;
|
||||
AMTDeviceGUID?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* TS reference of endpoint_create.go#EndpointCreationType iota
|
||||
|
@ -87,6 +90,12 @@ export enum EnvironmentCreationTypes {
|
|||
|
||||
export type EnvironmentGroupId = number;
|
||||
|
||||
export enum PlatformType {
|
||||
Docker,
|
||||
Kubernetes,
|
||||
Azure,
|
||||
}
|
||||
|
||||
export interface EnvironmentSettings {
|
||||
// Whether non-administrator should be able to use bind mounts when creating containers
|
||||
allowBindMountsForRegularUsers: boolean;
|
||||
|
|
44
app/portainer/environments/utils.ts
Normal file
44
app/portainer/environments/utils.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { Environment, EnvironmentType, PlatformType } from './types';
|
||||
|
||||
export function getPlatformType(envType: EnvironmentType) {
|
||||
switch (envType) {
|
||||
case EnvironmentType.KubernetesLocal:
|
||||
case EnvironmentType.AgentOnKubernetes:
|
||||
case EnvironmentType.EdgeAgentOnKubernetes:
|
||||
return PlatformType.Kubernetes;
|
||||
case EnvironmentType.Docker:
|
||||
case EnvironmentType.AgentOnDocker:
|
||||
case EnvironmentType.EdgeAgentOnDocker:
|
||||
return PlatformType.Docker;
|
||||
case EnvironmentType.Azure:
|
||||
return PlatformType.Azure;
|
||||
default:
|
||||
throw new Error(`Environment Type ${envType} is not supported`);
|
||||
}
|
||||
}
|
||||
|
||||
export function isEdgeEnvironment(envType: EnvironmentType) {
|
||||
return [
|
||||
EnvironmentType.EdgeAgentOnDocker,
|
||||
EnvironmentType.EdgeAgentOnKubernetes,
|
||||
].includes(envType);
|
||||
}
|
||||
|
||||
export function getRoute(environment: Environment) {
|
||||
if (isEdgeEnvironment(environment.Type) && !environment.EdgeID) {
|
||||
return 'portainer.endpoints.endpoint';
|
||||
}
|
||||
|
||||
const platform = getPlatformType(environment.Type);
|
||||
|
||||
switch (platform) {
|
||||
case PlatformType.Azure:
|
||||
return 'azure.dashboard';
|
||||
case PlatformType.Docker:
|
||||
return 'docker.dashboard';
|
||||
case PlatformType.Kubernetes:
|
||||
return 'kubernetes.dashboard';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
|
@ -12,6 +12,10 @@ export default class EndpointHelper {
|
|||
return endpoint.URL.includes('unix://') || endpoint.URL.includes('npipe://') || endpoint.Type === PortainerEndpointTypes.KubernetesLocalEnvironment;
|
||||
}
|
||||
|
||||
static isDockerEndpoint(endpoint) {
|
||||
return [PortainerEndpointTypes.DockerEnvironment, PortainerEndpointTypes.AgentOnDockerEnvironment, PortainerEndpointTypes.EdgeAgentOnDockerEnvironment].includes(endpoint.Type);
|
||||
}
|
||||
|
||||
static isAgentEndpoint(endpoint) {
|
||||
return [
|
||||
PortainerEndpointTypes.AgentOnDockerEnvironment,
|
||||
|
|
99
app/portainer/hostmanagement/fdo/fdo.service.ts
Normal file
99
app/portainer/hostmanagement/fdo/fdo.service.ts
Normal file
|
@ -0,0 +1,99 @@
|
|||
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');
|
||||
}
|
||||
}
|
20
app/portainer/hostmanagement/fdo/model.ts
Normal file
20
app/portainer/hostmanagement/fdo/model.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
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;
|
||||
};
|
40
app/portainer/hostmanagement/open-amt/model.ts
Normal file
40
app/portainer/hostmanagement/open-amt/model.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
export interface OpenAMTConfiguration {
|
||||
enabled: boolean;
|
||||
mpsServer: string;
|
||||
mpsUser: string;
|
||||
mpsPassword: string;
|
||||
domainName: string;
|
||||
certFileName: string;
|
||||
certFileContent: string;
|
||||
certFilePassword: string;
|
||||
}
|
||||
|
||||
export interface AMTInformation {
|
||||
uuid: string;
|
||||
amt: string;
|
||||
buildNumber: string;
|
||||
controlMode: string;
|
||||
dnsSuffix: string;
|
||||
rawOutput: string;
|
||||
}
|
||||
|
||||
export interface AuthorizationResponse {
|
||||
server: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface DeviceFeatures {
|
||||
ider: boolean;
|
||||
kvm: boolean;
|
||||
sol: boolean;
|
||||
redirection: boolean;
|
||||
userConsent: string;
|
||||
}
|
||||
|
||||
export type Device = {
|
||||
guid: string;
|
||||
hostname: string;
|
||||
powerState: number;
|
||||
connectionStatus: boolean;
|
||||
features?: DeviceFeatures;
|
||||
};
|
90
app/portainer/hostmanagement/open-amt/open-amt.service.ts
Normal file
90
app/portainer/hostmanagement/open-amt/open-amt.service.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import axios, { parseAxiosError } from 'Portainer/services/axios';
|
||||
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
|
||||
import {
|
||||
OpenAMTConfiguration,
|
||||
AMTInformation,
|
||||
AuthorizationResponse,
|
||||
Device,
|
||||
DeviceFeatures,
|
||||
} from './model';
|
||||
|
||||
const BASE_URL = '/open_amt';
|
||||
|
||||
export async function configureAMT(formValues: OpenAMTConfiguration) {
|
||||
try {
|
||||
await axios.post(`${BASE_URL}/configure`, formValues);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to configure AMT');
|
||||
}
|
||||
}
|
||||
|
||||
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 activateDevice(environmentId: EnvironmentId) {
|
||||
try {
|
||||
await axios.post(`${BASE_URL}/${environmentId}/activate`);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to activate device');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDevices(environmentId: EnvironmentId) {
|
||||
try {
|
||||
const { data: devices } = await axios.get<Device[]>(
|
||||
`${BASE_URL}/${environmentId}/devices`
|
||||
);
|
||||
|
||||
return devices;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to retrieve device information');
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeDeviceAction(
|
||||
environmentId: EnvironmentId,
|
||||
deviceGUID: string,
|
||||
action: string
|
||||
) {
|
||||
try {
|
||||
const actionPayload = { action };
|
||||
await axios.post(
|
||||
`${BASE_URL}/${environmentId}/devices/${deviceGUID}/action`,
|
||||
actionPayload
|
||||
);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to execute device action');
|
||||
}
|
||||
}
|
||||
|
||||
export async function enableDeviceFeatures(
|
||||
environmentId: EnvironmentId,
|
||||
deviceGUID: string,
|
||||
features: DeviceFeatures
|
||||
) {
|
||||
try {
|
||||
const featuresPayload = { features };
|
||||
const { data: authorizationResponse } =
|
||||
await axios.post<AuthorizationResponse>(
|
||||
`${BASE_URL}/${environmentId}/devices/${deviceGUID}/features`,
|
||||
featuresPayload
|
||||
);
|
||||
return authorizationResponse;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to enable device features');
|
||||
}
|
||||
}
|
|
@ -4,7 +4,8 @@ export function SettingsViewModel(data) {
|
|||
this.AuthenticationMethod = data.AuthenticationMethod;
|
||||
this.LDAPSettings = data.LDAPSettings;
|
||||
this.OAuthSettings = new OAuthSettingsViewModel(data.OAuthSettings);
|
||||
this.OpenAMTConfiguration = data.OpenAMTConfiguration;
|
||||
this.openAMTConfiguration = data.openAMTConfiguration;
|
||||
this.fdoConfiguration = data.fdoConfiguration;
|
||||
this.SnapshotInterval = data.SnapshotInterval;
|
||||
this.TemplatesURL = data.TemplatesURL;
|
||||
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;
|
||||
|
@ -14,11 +15,15 @@ export function SettingsViewModel(data) {
|
|||
this.EnableTelemetry = data.EnableTelemetry;
|
||||
this.KubeconfigExpiry = data.KubeconfigExpiry;
|
||||
this.HelmRepositoryURL = data.HelmRepositoryURL;
|
||||
this.DisableTrustOnFirstConnect = data.DisableTrustOnFirstConnect;
|
||||
this.EnforceEdgeID = data.EnforceEdgeID;
|
||||
}
|
||||
|
||||
export function PublicSettingsViewModel(settings) {
|
||||
this.AuthenticationMethod = settings.AuthenticationMethod;
|
||||
this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
||||
this.DisableTrustOnFirstConnect = settings.DisableTrustOnFirstConnect;
|
||||
this.EnforceEdgeID = settings.EnforceEdgeID;
|
||||
this.FeatureFlagSettings = settings.FeatureFlagSettings;
|
||||
this.LogoURL = settings.LogoURL;
|
||||
this.OAuthLoginURI = settings.OAuthLoginURI;
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import angular from 'angular';
|
||||
|
||||
const API_ENDPOINT_OPEN_AMT = 'api/open_amt';
|
||||
|
||||
angular.module('portainer.app').factory('OpenAMT', OpenAMTFactory);
|
||||
|
||||
/* @ngInject */
|
||||
function OpenAMTFactory($resource) {
|
||||
return $resource(
|
||||
API_ENDPOINT_OPEN_AMT,
|
||||
{},
|
||||
{
|
||||
submit: { method: 'POST' },
|
||||
}
|
||||
);
|
||||
}
|
|
@ -103,7 +103,8 @@ angular.module('portainer.app').factory('EndpointService', [
|
|||
TLSCAFile,
|
||||
TLSCertFile,
|
||||
TLSKeyFile,
|
||||
checkinInterval
|
||||
checkinInterval,
|
||||
isEdgeDevice
|
||||
) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
|
@ -125,7 +126,8 @@ angular.module('portainer.app').factory('EndpointService', [
|
|||
TLSCAFile,
|
||||
TLSCertFile,
|
||||
TLSKeyFile,
|
||||
checkinInterval
|
||||
checkinInterval,
|
||||
isEdgeDevice
|
||||
)
|
||||
.then(function success(response) {
|
||||
deferred.resolve(response.data);
|
||||
|
@ -165,6 +167,10 @@ angular.module('portainer.app').factory('EndpointService', [
|
|||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.trust = function (id) {
|
||||
Endpoints.updateEndpoint({ id }, { UserTrusted: true }).$promise;
|
||||
};
|
||||
|
||||
function updateRegistryAccess(id, registryId, endpointAccesses) {
|
||||
return Endpoints.updateRegistryAccess({ registryId, id }, endpointAccesses).$promise;
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.app').service('OpenAMTService', OpenAMTServiceFactory);
|
||||
|
||||
/* @ngInject */
|
||||
function OpenAMTServiceFactory(OpenAMT) {
|
||||
return {
|
||||
submit,
|
||||
};
|
||||
|
||||
function submit(formValues) {
|
||||
return OpenAMT.submit(formValues).$promise;
|
||||
}
|
||||
}
|
|
@ -136,7 +136,8 @@ angular.module('portainer.app').factory('FileUploadService', [
|
|||
TLSCAFile,
|
||||
TLSCertFile,
|
||||
TLSKeyFile,
|
||||
checkinInterval
|
||||
checkinInterval,
|
||||
isEdgeDevice
|
||||
) {
|
||||
return Upload.upload({
|
||||
url: 'api/endpoints',
|
||||
|
@ -154,6 +155,7 @@ angular.module('portainer.app').factory('FileUploadService', [
|
|||
TLSCertFile: TLSCertFile,
|
||||
TLSKeyFile: TLSKeyFile,
|
||||
CheckinInterval: checkinInterval,
|
||||
IsEdgeDevice: isEdgeDevice,
|
||||
},
|
||||
ignoreLoadingBar: true,
|
||||
});
|
||||
|
@ -207,6 +209,16 @@ angular.module('portainer.app').factory('FileUploadService', [
|
|||
return $q.all(queue);
|
||||
};
|
||||
|
||||
service.uploadOwnershipVoucher = function (voucherFile) {
|
||||
return Upload.upload({
|
||||
url: 'api/fdo/register',
|
||||
data: {
|
||||
voucher: voucherFile,
|
||||
},
|
||||
ignoreLoadingBar: true,
|
||||
});
|
||||
};
|
||||
|
||||
return service;
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -30,6 +30,15 @@ interface PromptOptions {
|
|||
callback: PromptCallback;
|
||||
}
|
||||
|
||||
export async function promptAsync(options: Omit<PromptOptions, 'callback'>) {
|
||||
return new Promise((resolve) => {
|
||||
prompt({
|
||||
...options,
|
||||
callback: (result: string | string[]) => resolve(result),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function prompt(options: PromptOptions) {
|
||||
const box = bootbox.prompt({
|
||||
title: options.title,
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
import { useTable, usePagination, useSortBy } from 'react-table';
|
||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||
import { FDOProfilesDatatableActions } from 'Portainer/settings/edge-compute/FDOProfilesDatatable/FDOProfilesDatatableActions';
|
||||
import { SelectedRowsCount } from 'Portainer/components/datatables/components/SelectedRowsCount';
|
||||
import { PaginationControls } from 'Portainer/components/pagination-controls';
|
||||
import { TableFooter } from 'Portainer/components/datatables/components/TableFooter';
|
||||
import { useTableSettings } from 'Portainer/components/datatables/components/useTableSettings';
|
||||
import { useRowSelect } from 'Portainer/components/datatables/components/useRowSelect';
|
||||
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
import PortainerError from '@/portainer/error';
|
||||
import {
|
||||
Table,
|
||||
TableActions,
|
||||
TableContainer,
|
||||
TableHeaderRow,
|
||||
TableRow,
|
||||
TableTitle,
|
||||
} from '@/portainer/components/datatables/components';
|
||||
import { FDOProfilesTableSettings } from '@/edge/devices/types';
|
||||
import { useFDOProfiles } from '@/portainer/settings/edge-compute/FDOProfilesDatatable/useFDOProfiles';
|
||||
|
||||
import { useColumns } from './columns';
|
||||
|
||||
export interface FDOProfilesDatatableProps {
|
||||
isFDOEnabled: boolean;
|
||||
}
|
||||
|
||||
export function FDOProfilesDatatable({
|
||||
isFDOEnabled,
|
||||
}: FDOProfilesDatatableProps) {
|
||||
const { settings, setTableSettings } =
|
||||
useTableSettings<FDOProfilesTableSettings>();
|
||||
const columns = useColumns();
|
||||
|
||||
const { isLoading, profiles, error } = useFDOProfiles();
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
page,
|
||||
prepareRow,
|
||||
selectedFlatRows,
|
||||
gotoPage,
|
||||
setPageSize,
|
||||
state: { pageIndex, pageSize },
|
||||
} = useTable<Profile>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: profiles,
|
||||
initialState: {
|
||||
pageSize: settings.pageSize || 10,
|
||||
sortBy: [settings.sortBy],
|
||||
},
|
||||
isRowSelectable() {
|
||||
return isFDOEnabled;
|
||||
},
|
||||
selectColumnWidth: 5,
|
||||
},
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
useRowSelectColumn
|
||||
);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<TableTitle icon="" label="Device Profiles" />
|
||||
|
||||
<TableActions>
|
||||
<FDOProfilesDatatableActions
|
||||
isFDOEnabled={isFDOEnabled}
|
||||
selectedItems={selectedFlatRows.map((row) => row.original)}
|
||||
/>
|
||||
</TableActions>
|
||||
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<TableHeaderRow<Profile>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
{!isLoading && profiles && profiles.length > 0 ? (
|
||||
page.map((row) => {
|
||||
prepareRow(row);
|
||||
const { key, className, role, style } = row.getRowProps();
|
||||
|
||||
return (
|
||||
<TableRow<Profile>
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-center text-muted">
|
||||
{userMessage(isLoading, error)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<TableFooter>
|
||||
<SelectedRowsCount value={selectedFlatRows.length} />
|
||||
<PaginationControls
|
||||
showAll
|
||||
pageLimit={pageSize}
|
||||
page={pageIndex + 1}
|
||||
onPageChange={(p) => gotoPage(p - 1)}
|
||||
totalCount={profiles ? profiles.length : 0}
|
||||
onPageLimitChange={handlePageSizeChange}
|
||||
/>
|
||||
</TableFooter>
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
setPageSize(pageSize);
|
||||
setTableSettings((settings) => ({ ...settings, pageSize }));
|
||||
}
|
||||
}
|
||||
|
||||
function userMessage(isLoading: boolean, error?: PortainerError) {
|
||||
if (isLoading) {
|
||||
return 'Loading...';
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return 'No profiles found';
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
import { useQueryClient } from 'react-query';
|
||||
import { useRouter } from '@uirouter/react';
|
||||
|
||||
import { Button } from '@/portainer/components/Button';
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
import { Link } from '@/portainer/components/Link';
|
||||
import { confirmAsync } from '@/portainer/services/modal.service/confirm';
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import {
|
||||
deleteProfile,
|
||||
duplicateProfile,
|
||||
} from '@/portainer/hostmanagement/fdo/fdo.service';
|
||||
|
||||
interface Props {
|
||||
isFDOEnabled: boolean;
|
||||
selectedItems: Profile[];
|
||||
}
|
||||
|
||||
export function FDOProfilesDatatableActions({
|
||||
isFDOEnabled,
|
||||
selectedItems,
|
||||
}: Props) {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return (
|
||||
<div className="actionBar">
|
||||
<Link to="portainer.endpoints.profile" className="space-left">
|
||||
<Button disabled={!isFDOEnabled}>
|
||||
<i className="fa fa-plus-circle space-right" aria-hidden="true" />
|
||||
Add Profile
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Button
|
||||
disabled={!isFDOEnabled || selectedItems.length !== 1}
|
||||
onClick={() => onDuplicateProfileClick()}
|
||||
>
|
||||
<i className="fa fa-plus-circle space-right" aria-hidden="true" />
|
||||
Duplicate
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
disabled={!isFDOEnabled || selectedItems.length < 1}
|
||||
color="danger"
|
||||
onClick={() => onDeleteProfileClick()}
|
||||
>
|
||||
<i className="fa fa-trash-alt space-right" aria-hidden="true" />
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
async function onDuplicateProfileClick() {
|
||||
const confirmed = await confirmAsync({
|
||||
title: 'Are you sure ?',
|
||||
message: 'This action will duplicate the selected profile. Continue?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Confirm',
|
||||
className: 'btn-primary',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = selectedItems[0];
|
||||
const newProfile = await duplicateProfile(profile.id);
|
||||
console.log({ newProfile });
|
||||
notifications.success('Profile successfully duplicated', profile.name);
|
||||
router.stateService.go('portainer.endpoints.profile.edit', {
|
||||
id: newProfile.id,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
notifications.error(
|
||||
'Failure',
|
||||
err as Error,
|
||||
'Unable to duplicate profile'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeleteProfileClick() {
|
||||
const confirmed = await confirmAsync({
|
||||
title: 'Are you sure ?',
|
||||
message: 'This action will delete the selected profile(s). Continue?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Remove',
|
||||
className: 'btn-danger',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { TableSettingsProvider } from '@/portainer/components/datatables/components/useTableSettings';
|
||||
|
||||
import {
|
||||
FDOProfilesDatatable,
|
||||
FDOProfilesDatatableProps,
|
||||
} from './FDOProfilesDatatable';
|
||||
|
||||
export function FDOProfilesDatatableContainer({
|
||||
...props
|
||||
}: FDOProfilesDatatableProps) {
|
||||
const defaultSettings = {
|
||||
pageSize: 10,
|
||||
sortBy: { id: 'name', desc: false },
|
||||
};
|
||||
|
||||
return (
|
||||
<TableSettingsProvider defaults={defaultSettings} storageKey="fdoProfiles">
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<FDOProfilesDatatable {...props} />
|
||||
</TableSettingsProvider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { Column } from 'react-table';
|
||||
|
||||
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
|
||||
export const created: Column<Profile> = {
|
||||
Header: 'Created',
|
||||
accessor: 'dateCreated',
|
||||
id: 'created',
|
||||
Cell: ({ value }) => isoDateFromTimestamp(value),
|
||||
disableFilters: true,
|
||||
canHide: true,
|
||||
Filter: () => null,
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
import { created } from './created';
|
||||
import { name } from './name';
|
||||
|
||||
export function useColumns() {
|
||||
return useMemo(() => [name, created], []);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { CellProps, Column } from 'react-table';
|
||||
import { useSref } from '@uirouter/react';
|
||||
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
|
||||
export const name: Column<Profile> = {
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
id: 'name',
|
||||
Cell: NameCell,
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: true,
|
||||
sortType: 'string',
|
||||
};
|
||||
|
||||
export function NameCell({
|
||||
value: name,
|
||||
row: { original: profile },
|
||||
}: CellProps<Profile>) {
|
||||
const linkProps = useSref('portainer.endpoints.profile.edit', {
|
||||
id: profile.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<a href={linkProps.href} onClick={linkProps.onClick} title={name}>
|
||||
{name}
|
||||
</a>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
import { useQuery } from '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,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.save-button {
|
||||
margin-top: 2rem;
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
import { Formik, Form } from 'formik';
|
||||
|
||||
import { Switch } from '@/portainer/components/form-components/SwitchField/Switch';
|
||||
import { FormControl } from '@/portainer/components/form-components/FormControl';
|
||||
import { Select } from '@/portainer/components/form-components/Input/Select';
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget';
|
||||
import { LoadingButton } from '@/portainer/components/Button/LoadingButton';
|
||||
import { TextTip } from '@/portainer/components/Tip/TextTip';
|
||||
|
||||
import styles from './SettingsEdgeCompute.module.css';
|
||||
import { validationSchema } from './SettingsEdgeCompute.validation';
|
||||
|
||||
export interface FormValues {
|
||||
EdgeAgentCheckinInterval: number;
|
||||
EnableEdgeComputeFeatures: boolean;
|
||||
DisableTrustOnFirstConnect: boolean;
|
||||
EnforceEdgeID: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
settings: FormValues;
|
||||
onSubmit(values: FormValues): void;
|
||||
}
|
||||
|
||||
const checkinIntervalOptions = [
|
||||
{
|
||||
value: 5,
|
||||
label: '5 seconds',
|
||||
},
|
||||
{
|
||||
value: 10,
|
||||
label: '10 seconds',
|
||||
},
|
||||
{
|
||||
value: 30,
|
||||
label: '30 seconds',
|
||||
},
|
||||
];
|
||||
|
||||
export function SettingsEdgeCompute({ settings, onSubmit }: Props) {
|
||||
const initialValues = {
|
||||
EdgeAgentCheckinInterval: settings ? settings.EdgeAgentCheckinInterval : 5,
|
||||
EnableEdgeComputeFeatures: settings
|
||||
? settings.EnableEdgeComputeFeatures
|
||||
: false,
|
||||
DisableTrustOnFirstConnect: settings
|
||||
? settings.DisableTrustOnFirstConnect
|
||||
: false,
|
||||
EnforceEdgeID: settings ? settings.EnforceEdgeID : false,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<Widget>
|
||||
<WidgetTitle icon="fa-laptop" title="Edge Compute settings" />
|
||||
<WidgetBody>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
enableReinitialize
|
||||
validationSchema={() => validationSchema()}
|
||||
onSubmit={onSubmit}
|
||||
validateOnMount
|
||||
>
|
||||
{({
|
||||
values,
|
||||
errors,
|
||||
handleSubmit,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
dirty,
|
||||
}) => (
|
||||
<Form
|
||||
className="form-horizontal"
|
||||
onSubmit={handleSubmit}
|
||||
noValidate
|
||||
>
|
||||
<FormControl
|
||||
inputId="edge_checkin"
|
||||
label="Edge agent default poll frequency"
|
||||
size="medium"
|
||||
tooltip="Interval used by default by each Edge agent to check in with the Portainer instance. Affects Edge environment management and Edge compute features."
|
||||
errors={errors.EdgeAgentCheckinInterval}
|
||||
>
|
||||
<Select
|
||||
value={values.EdgeAgentCheckinInterval}
|
||||
onChange={(e) =>
|
||||
setFieldValue(
|
||||
'EdgeAgentCheckinInterval',
|
||||
parseInt(e.currentTarget.value, 10)
|
||||
)
|
||||
}
|
||||
options={checkinIntervalOptions}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
inputId="edge_checkin"
|
||||
label="Enable Edge Compute features"
|
||||
size="medium"
|
||||
errors={errors.EnableEdgeComputeFeatures}
|
||||
>
|
||||
<Switch
|
||||
id="edge_enable"
|
||||
name="edge_enable"
|
||||
className="space-right"
|
||||
checked={values.EnableEdgeComputeFeatures}
|
||||
onChange={(e) =>
|
||||
setFieldValue('EnableEdgeComputeFeatures', e)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<TextTip color="blue">
|
||||
When enabled, this will enable Portainer to execute Edge
|
||||
Device features.
|
||||
</TextTip>
|
||||
|
||||
<FormControl
|
||||
inputId="edge_enforce_id"
|
||||
label="Enforce environment ID"
|
||||
size="medium"
|
||||
errors={errors.EnforceEdgeID}
|
||||
>
|
||||
<Switch
|
||||
id="edge_enforce_id"
|
||||
name="edge_enforce_id"
|
||||
className="space-right"
|
||||
checked={values.EnforceEdgeID}
|
||||
onChange={(e) =>
|
||||
setFieldValue('EnforceEdgeID', e.valueOf())
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
inputId="edge_tofc"
|
||||
label="Trust on first connect"
|
||||
size="medium"
|
||||
errors={errors.DisableTrustOnFirstConnect}
|
||||
>
|
||||
<Switch
|
||||
id="edge_disable_tofc"
|
||||
name="edge_disable_tofc"
|
||||
className="space-right"
|
||||
checked={!values.DisableTrustOnFirstConnect}
|
||||
onChange={(e) =>
|
||||
setFieldValue('DisableTrustOnFirstConnect', !e.valueOf())
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
disabled={!isValid || !dirty}
|
||||
dataCy="settings-edgeComputeButton"
|
||||
className={styles.saveButton}
|
||||
isLoading={isSubmitting}
|
||||
loadingText="Saving settings..."
|
||||
>
|
||||
Save Settings
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { boolean, number, object } from 'yup';
|
||||
|
||||
export function validationSchema() {
|
||||
return object().shape({
|
||||
EdgeAgentCheckinInterval: number().required('This field is required.'),
|
||||
EnableEdgeComputeFeatures: boolean().required('This field is required.'),
|
||||
DisableTrustOnFirstConnect: boolean().required('This field is required.'),
|
||||
EnforceEdgeID: boolean().required('This field is required.'),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { react2angular } from '@/react-tools/react2angular';
|
||||
|
||||
import { SettingsEdgeCompute } from './SettingsEdgeCompute';
|
||||
|
||||
const SettingsEdgeComputeAngular = react2angular(SettingsEdgeCompute, [
|
||||
'settings',
|
||||
'onSubmit',
|
||||
]);
|
||||
export { SettingsEdgeCompute, SettingsEdgeComputeAngular };
|
|
@ -0,0 +1,7 @@
|
|||
.save-button {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.fdo-table {
|
||||
margin-top: 3em;
|
||||
}
|
186
app/portainer/settings/edge-compute/SettingsFDO/SettingsFDO.tsx
Normal file
186
app/portainer/settings/edge-compute/SettingsFDO/SettingsFDO.tsx
Normal file
|
@ -0,0 +1,186 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Formik, Field, Form } from 'formik';
|
||||
|
||||
import { Switch } from '@/portainer/components/form-components/SwitchField/Switch';
|
||||
import { FormControl } from '@/portainer/components/form-components/FormControl';
|
||||
import { FormSectionTitle } from '@/portainer/components/form-components/FormSectionTitle';
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget';
|
||||
import { LoadingButton } from '@/portainer/components/Button/LoadingButton';
|
||||
import { TextTip } from '@/portainer/components/Tip/TextTip';
|
||||
import { Input } from '@/portainer/components/form-components/Input';
|
||||
import { FDOConfiguration } from '@/portainer/hostmanagement/fdo/model';
|
||||
import { FDOProfilesDatatableContainer } from '@/portainer/settings/edge-compute/FDOProfilesDatatable/FDOProfilesDatatableContainer';
|
||||
|
||||
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 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="fa-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="medium"
|
||||
errors={errors.enabled}
|
||||
>
|
||||
<Switch
|
||||
id="edge_enableFDO"
|
||||
name="edge_enableFDO"
|
||||
className="space-right"
|
||||
disabled={!edgeComputeFeaturesEnabled}
|
||||
checked={edgeComputeFeaturesEnabled && values.enabled}
|
||||
onChange={(e) => onChangedEnabled(e, setFieldValue)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<TextTip color="blue">
|
||||
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">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
disabled={!isValid || !dirty}
|
||||
dataCy="settings-fdoButton"
|
||||
className={styles.saveButton}
|
||||
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">
|
||||
Add, Edit and Manage the list of device profiles available
|
||||
during FDO device setup
|
||||
</TextTip>
|
||||
<FDOProfilesDatatableContainer isFDOEnabled={initialFDOEnabled} />
|
||||
</div>
|
||||
)}
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
);
|
||||
|
||||
async function onChangedEnabled(
|
||||
e: boolean,
|
||||
setFieldValue: (
|
||||
field: string,
|
||||
value: unknown,
|
||||
shouldValidate?: boolean
|
||||
) => void
|
||||
) {
|
||||
setIsFDOEnabled(e);
|
||||
setFieldValue('enabled', e);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
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'),
|
||||
}),
|
||||
});
|
||||
}
|
6
app/portainer/settings/edge-compute/SettingsFDO/index.ts
Normal file
6
app/portainer/settings/edge-compute/SettingsFDO/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { react2angular } from '@/react-tools/react2angular';
|
||||
|
||||
import { SettingsFDO } from './SettingsFDO';
|
||||
|
||||
const SettingsFDOAngular = react2angular(SettingsFDO, ['settings', 'onSubmit']);
|
||||
export { SettingsFDO, SettingsFDOAngular };
|
|
@ -0,0 +1,3 @@
|
|||
.save-button {
|
||||
margin-top: 2rem;
|
||||
}
|
|
@ -0,0 +1,266 @@
|
|||
import { useState } from 'react';
|
||||
import { Formik, Field, Form } from 'formik';
|
||||
|
||||
import { Switch } from '@/portainer/components/form-components/SwitchField/Switch';
|
||||
import { FormControl } from '@/portainer/components/form-components/FormControl';
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget';
|
||||
import { LoadingButton } from '@/portainer/components/Button/LoadingButton';
|
||||
import { TextTip } from '@/portainer/components/Tip/TextTip';
|
||||
import { Input } from '@/portainer/components/form-components/Input';
|
||||
import { FileUploadField } from '@/portainer/components/form-components/FileUpload';
|
||||
import { OpenAMTConfiguration } from '@/portainer/hostmanagement/open-amt/model';
|
||||
|
||||
import styles from './SettingsOpenAMT.module.css';
|
||||
import { validationSchema } from './SettingsOpenAMT.validation';
|
||||
|
||||
export interface Settings {
|
||||
openAMTConfiguration: OpenAMTConfiguration;
|
||||
EnableEdgeComputeFeatures: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
onSubmit(values: OpenAMTConfiguration): void;
|
||||
}
|
||||
|
||||
export function SettingsOpenAMT({ settings, onSubmit }: Props) {
|
||||
const [certFile, setCertFile] = useState<File>();
|
||||
async function handleFileUpload(
|
||||
file: File,
|
||||
setFieldValue: (
|
||||
field: string,
|
||||
value: unknown,
|
||||
shouldValidate?: boolean
|
||||
) => void
|
||||
) {
|
||||
if (file) {
|
||||
setCertFile(file);
|
||||
const fileContent = await readFileContent(file);
|
||||
setFieldValue('certFileContent', fileContent);
|
||||
setFieldValue('certFileName', file.name);
|
||||
}
|
||||
}
|
||||
|
||||
function readFileContent(file: File) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => {
|
||||
if (e.target == null || e.target.result == null) {
|
||||
resolve('');
|
||||
return;
|
||||
}
|
||||
const base64 = e.target.result.toString();
|
||||
// remove prefix of "data:application/x-pkcs12;base64," returned by "readAsDataURL()"
|
||||
const index = base64.indexOf('base64,');
|
||||
const cert = base64.substring(index + 7, base64.length);
|
||||
resolve(cert);
|
||||
};
|
||||
fileReader.onerror = () => {
|
||||
reject(new Error('error reading provisioning certificate file'));
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
const openAMTConfiguration = settings ? settings.openAMTConfiguration : null;
|
||||
const initialValues = {
|
||||
enabled: openAMTConfiguration ? openAMTConfiguration.enabled : false,
|
||||
mpsServer: openAMTConfiguration ? openAMTConfiguration.mpsServer : '',
|
||||
mpsUser: openAMTConfiguration ? openAMTConfiguration.mpsUser : '',
|
||||
mpsPassword: openAMTConfiguration ? openAMTConfiguration.mpsPassword : '',
|
||||
domainName: openAMTConfiguration ? openAMTConfiguration.domainName : '',
|
||||
certFileContent: openAMTConfiguration
|
||||
? openAMTConfiguration.certFileContent
|
||||
: '',
|
||||
certFileName: openAMTConfiguration ? openAMTConfiguration.certFileName : '',
|
||||
certFilePassword: openAMTConfiguration
|
||||
? openAMTConfiguration.certFilePassword
|
||||
: '',
|
||||
};
|
||||
|
||||
if (
|
||||
initialValues.certFileContent &&
|
||||
initialValues.certFileName &&
|
||||
!certFile
|
||||
) {
|
||||
setCertFile(new File([], initialValues.certFileName));
|
||||
}
|
||||
|
||||
const edgeComputeFeaturesEnabled = settings
|
||||
? settings.EnableEdgeComputeFeatures
|
||||
: false;
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<Widget>
|
||||
<WidgetTitle icon="fa-laptop" title="Intel OpenAMT" />
|
||||
<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_enableOpenAMT"
|
||||
label="Enable OpenAMT"
|
||||
errors={errors.enabled}
|
||||
>
|
||||
<Switch
|
||||
id="edge_enableOpenAMT"
|
||||
name="edge_enableOpenAMT"
|
||||
className="space-right"
|
||||
disabled={!edgeComputeFeaturesEnabled}
|
||||
checked={edgeComputeFeaturesEnabled && values.enabled}
|
||||
onChange={(e) => setFieldValue('enabled', e)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<TextTip color="blue">
|
||||
When enabled, this will allow Portainer to interact with an
|
||||
OpenAMT MPS API.
|
||||
</TextTip>
|
||||
|
||||
{edgeComputeFeaturesEnabled && values.enabled && (
|
||||
<>
|
||||
<hr />
|
||||
|
||||
<FormControl
|
||||
inputId="mps_server"
|
||||
label="MPS Server"
|
||||
size="medium"
|
||||
errors={errors.mpsServer}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
name="mpsServer"
|
||||
id="mps_server"
|
||||
placeholder="Enter the MPS Server"
|
||||
value={values.mpsServer}
|
||||
data-cy="openAMT-serverInput"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
inputId="mps_username"
|
||||
label="MPS User"
|
||||
size="medium"
|
||||
errors={errors.mpsUser}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
name="mpsUser"
|
||||
id="mps_username"
|
||||
placeholder="Enter the MPS User"
|
||||
value={values.mpsUser}
|
||||
data-cy="openAMT-usernameInput"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
inputId="mps_password"
|
||||
label="MPS Password"
|
||||
size="medium"
|
||||
tooltip="Needs to be 8-32 characters including one uppercase, one lowercase letters, one base-10 digit and one special character."
|
||||
errors={errors.mpsPassword}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
type="password"
|
||||
name="mpsPassword"
|
||||
id="mps_password"
|
||||
placeholder="Enter the MPS Password"
|
||||
value={values.mpsPassword}
|
||||
data-cy="openAMT-passwordInput"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<hr />
|
||||
|
||||
<FormControl
|
||||
inputId="domain_name"
|
||||
label="Domain Name"
|
||||
size="medium"
|
||||
tooltip="Enter the FQDN that is associated with the provisioning certificate (i.e amtdomain.com)"
|
||||
errors={errors.domainName}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
name="domainName"
|
||||
id="domain_name"
|
||||
placeholder="Enter the Domain Name"
|
||||
value={values.domainName}
|
||||
data-cy="openAMT-domainInput"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
inputId="certificate_file"
|
||||
label="Provisioning Certificate File (.pfx)"
|
||||
size="medium"
|
||||
tooltip="Supported CAs are Comodo, DigiCert, Entrust and GoDaddy.<br>The certificate must contain the private key.<br>On AMT 15 based devices you need to use SHA2."
|
||||
errors={errors.certFileContent}
|
||||
>
|
||||
<FileUploadField
|
||||
title="Upload file"
|
||||
accept=".pfx"
|
||||
value={certFile}
|
||||
onChange={(file) =>
|
||||
handleFileUpload(file, setFieldValue)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
inputId="certificate_password"
|
||||
label="Provisioning Certificate Password"
|
||||
size="medium"
|
||||
tooltip="Needs to be 8-32 characters including one uppercase, one lowercase letters, one base-10 digit and one special character."
|
||||
errors={errors.certFilePassword}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
type="password"
|
||||
name="certFilePassword"
|
||||
id="certificate_password"
|
||||
placeholder="**********"
|
||||
value={values.certFilePassword}
|
||||
data-cy="openAMT-certPasswordInput"
|
||||
/>
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
disabled={!isValid || !dirty}
|
||||
dataCy="settings-fdoButton"
|
||||
className={styles.saveButton}
|
||||
isLoading={isSubmitting}
|
||||
loadingText="Saving settings..."
|
||||
>
|
||||
Save Settings
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { object, string } from 'yup';
|
||||
|
||||
export function validationSchema() {
|
||||
return object().shape({
|
||||
mpsServer: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
mpsUser: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
mpsPassword: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
domainName: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
certFileContent: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
certFileName: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
certFilePassword: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { react2angular } from '@/react-tools/react2angular';
|
||||
|
||||
import { SettingsOpenAMT } from './SettingsOpenAMT';
|
||||
|
||||
const SettingsOpenAMTAngular = react2angular(SettingsOpenAMT, [
|
||||
'settings',
|
||||
'onSubmit',
|
||||
]);
|
||||
export { SettingsOpenAMT, SettingsOpenAMTAngular };
|
|
@ -1,6 +1,5 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { sslCertificate } from './ssl-certificate';
|
||||
import { openAMT } from './open-amt';
|
||||
|
||||
export default angular.module('portainer.settings.general', []).component('sslCertificateSettings', sslCertificate).component('openAmtSettings', openAMT).name;
|
||||
export default angular.module('portainer.settings.general', []).component('sslCertificateSettings', sslCertificate).name;
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import controller from './open-amt.controller.js';
|
||||
|
||||
export const openAMT = {
|
||||
templateUrl: './open-amt.html',
|
||||
controller,
|
||||
};
|
|
@ -1,118 +0,0 @@
|
|||
class OpenAmtController {
|
||||
/* @ngInject */
|
||||
constructor($async, $scope, $state, OpenAMTService, SettingsService, Notifications) {
|
||||
Object.assign(this, { $async, $scope, $state, OpenAMTService, SettingsService, Notifications });
|
||||
|
||||
this.originalValues = {};
|
||||
this.formValues = {
|
||||
enableOpenAMT: false,
|
||||
mpsServer: '',
|
||||
mpsUser: '',
|
||||
mpsPassword: '',
|
||||
domainName: '',
|
||||
certFile: null,
|
||||
certPassword: '',
|
||||
useWirelessConfig: false,
|
||||
wifiAuthenticationMethod: '4',
|
||||
wifiEncryptionMethod: '3',
|
||||
wifiSsid: '',
|
||||
wifiPskPass: '',
|
||||
};
|
||||
|
||||
this.originalValues = {
|
||||
...this.formValues,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
};
|
||||
|
||||
this.save = this.save.bind(this);
|
||||
this.onChangeEnableOpenAMT = this.onChangeEnableOpenAMT.bind(this);
|
||||
}
|
||||
|
||||
onChangeEnableOpenAMT(checked) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.formValues.enableOpenAMT = checked;
|
||||
});
|
||||
}
|
||||
|
||||
isFormChanged() {
|
||||
return Object.entries(this.originalValues).some(([key, value]) => value !== this.formValues[key]);
|
||||
}
|
||||
|
||||
isFormValid() {
|
||||
return !this.formValues.enableOpenAMT || this.formValues.certFile != null;
|
||||
}
|
||||
|
||||
async readFile() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = this.formValues.certFile;
|
||||
if (file) {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.fileName = file.name;
|
||||
fileReader.onload = (e) => {
|
||||
const base64 = e.target.result;
|
||||
// remove prefix of "data:application/x-pkcs12;base64," returned by "readAsDataURL()"
|
||||
const index = base64.indexOf('base64,');
|
||||
const cert = base64.substring(index + 7, base64.length);
|
||||
resolve(cert);
|
||||
};
|
||||
fileReader.onerror = () => {
|
||||
reject(new Error('error reading provisioning certificate file'));
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async save() {
|
||||
return this.$async(async () => {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
this.formValues.certFileText = this.formValues.certFile ? await this.readFile(this.formValues.certFile) : null;
|
||||
await this.OpenAMTService.submit(this.formValues);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
this.Notifications.success(`OpenAMT successfully ${this.formValues.enableOpenAMT ? 'enabled' : 'disabled'}`);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Failed applying changes');
|
||||
}
|
||||
this.state.actionInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
const data = await this.SettingsService.settings();
|
||||
const config = data.OpenAMTConfiguration;
|
||||
|
||||
if (config) {
|
||||
this.formValues = {
|
||||
...this.formValues,
|
||||
enableOpenAMT: config.Enabled,
|
||||
mpsServer: config.MPSServer,
|
||||
mpsUser: config.Credentials.MPSUser,
|
||||
domainName: config.DomainConfiguration.DomainName,
|
||||
};
|
||||
|
||||
if (config.WirelessConfiguration) {
|
||||
this.formValues.useWirelessConfig = true;
|
||||
this.formValues.wifiAuthenticationMethod = config.WirelessConfiguration.AuthenticationMethod;
|
||||
this.formValues.wifiEncryptionMethod = config.WirelessConfiguration.EncryptionMethod;
|
||||
this.formValues.wifiSsid = config.WirelessConfiguration.SSID;
|
||||
}
|
||||
|
||||
this.originalValues = {
|
||||
...this.formValues,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Failed loading settings');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default OpenAmtController;
|
|
@ -1,249 +0,0 @@
|
|||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-laptop" title-text="Intel OpenAMT"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="openAMTForm">
|
||||
<por-switch-field checked="$ctrl.formValues.enableOpenAMT" label="'Enable edge OpenAMT'" on-change="($ctrl.onChangeEnableOpenAMT)"></por-switch-field>
|
||||
<span class="small">
|
||||
<p class="text-muted" style="margin-top: 10px">
|
||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
When enabled, this will allow Portainer to interact with an OpenAMT MPS API.
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<div ng-show="$ctrl.formValues.enableOpenAMT">
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="mps_server" class="col-sm-3 control-label text-left"> MPS Server </label>
|
||||
<div class="col-sm-8">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="$ctrl.formValues.mpsServer"
|
||||
id="mps_server"
|
||||
name="mps_server"
|
||||
placeholder="Enter the MPS Server"
|
||||
ng-required="$ctrl.formValues.enableOpenAMT"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="openAMTForm.mps_server.$invalid">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="openAMTForm.mps_server.$error">
|
||||
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="mps_user" class="col-sm-3 control-label text-left"> MPS User </label>
|
||||
<div class="col-sm-8">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="$ctrl.formValues.mpsUser"
|
||||
id="mps_user"
|
||||
name="mps_user"
|
||||
placeholder="Enter the MPS User"
|
||||
ng-required="$ctrl.formValues.enableOpenAMT"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="openAMTForm.mps_user.$invalid">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="openAMTForm.mps_user.$error">
|
||||
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="mps_user" class="col-sm-3 control-label text-left">
|
||||
MPS Password
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="Needs to be 8-32 characters including one uppercase, one lowercase letters, one base-10 digit and one special character."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
ng-model="$ctrl.formValues.mpsPassword"
|
||||
id="mps_password"
|
||||
name="mps_password"
|
||||
placeholder="Enter the MPS Password"
|
||||
ng-required="$ctrl.formValues.enableOpenAMT"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="openAMTForm.mps_password.$invalid">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="openAMTForm.mps_password.$error">
|
||||
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="domain_name" class="col-sm-3 control-label text-left">
|
||||
Domain Name
|
||||
<portainer-tooltip position="bottom" message="Enter the FQDN that is associated with the provisioning certificate (i.e amtdomain.com)"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="$ctrl.formValues.domainName"
|
||||
id="domain_name"
|
||||
name="domain_name"
|
||||
placeholder="Enter the Domain Name"
|
||||
ng-required="$ctrl.formValues.enableOpenAMT"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="openAMTForm.domain_name.$invalid">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="openAMTForm.domain_name.$error">
|
||||
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="certificate_file" class="col-sm-3 control-label text-left">
|
||||
Provisioning Certificate File (.pfx)
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="Supported CAs are Comodo, DigiCert, Entrust and GoDaddy. The certificate must contain the private key."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<button style="margin-left: 0px !important" class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formValues.certFile" ngf-pattern=".pfx" name="certFile">
|
||||
Upload file
|
||||
</button>
|
||||
<span style="margin-left: 5px">
|
||||
{{ $ctrl.formValues.certFile.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!$ctrl.formValues.certFile" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="openAMTForm.certFile.$invalid">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="openAMTForm.certFile.$error">
|
||||
<p ng-message="pattern"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> File type is invalid.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="certificate_password" class="col-sm-3 control-label text-left">
|
||||
Provisioning Certificate Password
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="Needs to be 8-32 characters including one uppercase, one lowercase letters, one base-10 digit and one special character."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
ng-model="$ctrl.formValues.certPassword"
|
||||
id="certificate_password"
|
||||
name="certificate_password"
|
||||
placeholder="**********"
|
||||
ng-required="$ctrl.formValues.enableOpenAMT"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="openAMTForm.certificate_password.$invalid">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="openAMTForm.certificate_password.$error">
|
||||
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<!-- <por-switch-field checked="$ctrl.formValues.useWirelessConfig" label="'Wireless Configuration'" on-change="$ctrl.onChangeUseWirelessConfig"></por-switch-field> -->
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="$ctrl.formValues.useWirelessConfig">
|
||||
<div class="form-group">
|
||||
<label for="wifi_auth_method" class="col-sm-3 control-label text-left"> Authentication Method </label>
|
||||
<div class="col-sm-8">
|
||||
<select class="form-control" ng-model="$ctrl.formValues.wifiAuthenticationMethod">
|
||||
<option value="4">WPA PSK</option>
|
||||
<option value="6">WPA2 PSK</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="wifi_encrypt_method" class="col-sm-3 control-label text-left"> Encryption Method </label>
|
||||
<div class="col-sm-8">
|
||||
<select class="form-control" ng-model="$ctrl.formValues.wifiEncryptionMethod" id="wifi_encrypt_method">
|
||||
<option value="3">TKIP</option>
|
||||
<option value="4">CCMP</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="wifi_ssid" class="col-sm-3 control-label text-left"> SSID </label>
|
||||
<div class="col-sm-8">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="$ctrl.formValues.wifiSsid"
|
||||
id="wifi_ssid"
|
||||
placeholder="SSIID"
|
||||
ng-required="$ctrl.formValues.enableOpenAMT && $ctrl.formValues.useWirelessConfig"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="wifi_pass" class="col-sm-3 control-label text-left">
|
||||
PSK Passphrase
|
||||
<portainer-tooltip position="bottom" message="PSK Passphrase length should be greater than or equal to 8 and less than or equal to 63"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
ng-model="$ctrl.formValues.wifiPskPass"
|
||||
id="wifi_pass"
|
||||
placeholder="******"
|
||||
ng-required="$ctrl.formValues.enableOpenAMT && $ctrl.formValues.useWirelessConfig"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="$ctrl.state.actionInProgress || !openAMTForm.$valid || !$ctrl.isFormChanged() || !$ctrl.isFormValid()"
|
||||
ng-click="$ctrl.save()"
|
||||
button-spinner="$ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="$ctrl.state.actionInProgress">Save Settings</span>
|
||||
<span ng-show="$ctrl.state.actionInProgress">In progress...</span>
|
||||
</button>
|
||||
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px">{{ state.formValidationError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -3,4 +3,12 @@ import angular from 'angular';
|
|||
import authenticationModule from './authentication';
|
||||
import generalModule from './general';
|
||||
|
||||
export default angular.module('portainer.settings', [authenticationModule, generalModule]).name;
|
||||
import { SettingsEdgeComputeAngular } from './edge-compute/SettingsEdgeCompute';
|
||||
import { SettingsFDOAngular } from './edge-compute/SettingsFDO';
|
||||
import { SettingsOpenAMTAngular } from './edge-compute/SettingsOpenAMT';
|
||||
|
||||
export default angular
|
||||
.module('portainer.settings', [authenticationModule, generalModule])
|
||||
.component('settingsEdgeCompute', SettingsEdgeComputeAngular)
|
||||
.component('settingsFdo', SettingsFDOAngular)
|
||||
.component('settingsOpenAmt', SettingsOpenAMTAngular).name;
|
||||
|
|
220
app/portainer/views/devices/import/importDevice.html
Normal file
220
app/portainer/views/devices/import/importDevice.html
Normal file
|
@ -0,0 +1,220 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="FDO Device Configuration"></rd-header-title>
|
||||
<rd-header-content> <a ui-sref="portainer.endpoints">Environments</a> > Import FDO Device </rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-magic" 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">
|
||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
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">
|
||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
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 <i class="fa fa-upload" aria-hidden="true" style="margin-left: 5px"></i></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 Manufacturer's Rendezvous Server...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="state.vouchersUploaded">
|
||||
<div class="col-sm-12">
|
||||
<p>Ownership Voucher Uploaded <i class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 5px"></i></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">
|
||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
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"
|
||||
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
|
||||
data-cy="deviceImport-deviceNameInput"
|
||||
/>
|
||||
</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"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !device name input -->
|
||||
<!-- suffix input -->
|
||||
<span class="small">
|
||||
<p class="text-muted" style="margin-top: 10px">
|
||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
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"
|
||||
class="form-control"
|
||||
name="suffix"
|
||||
ng-model="formValues.Suffix"
|
||||
ng-required="state.vouchersUploaded"
|
||||
ng-disabled="!state.vouchersUploaded"
|
||||
ng-pattern="/^[0-9]+$/"
|
||||
placeholder="1"
|
||||
required
|
||||
data-cy="deviceImport-suffixInput"
|
||||
/>
|
||||
</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"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
<p ng-message="pattern"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> 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 position="bottom" 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"
|
||||
class="form-control"
|
||||
name="endpoint_url"
|
||||
ng-model="formValues.PortainerURL"
|
||||
ng-required="state.vouchersUploaded"
|
||||
ng-disabled="!state.vouchersUploaded"
|
||||
placeholder="e.g. http://10.0.0.10:9443"
|
||||
required
|
||||
data-cy="deviceImport-portainerServerUrlInput"
|
||||
/>
|
||||
</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"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> 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" 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">
|
||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
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"
|
||||
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 -->
|
||||
<!-- tags -->
|
||||
<div class="form-group">
|
||||
<tag-selector ng-if="formValues && availableTags" tags="availableTags" model="formValues.TagIds" allow-create="state.allowCreateTag" on-create="(onCreateTag)">
|
||||
</tag-selector>
|
||||
</div>
|
||||
<!-- !tags -->
|
||||
<!-- 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>
|
144
app/portainer/views/devices/import/importDeviceController.js
Normal file
144
app/portainer/views/devices/import/importDeviceController.js
Normal file
|
@ -0,0 +1,144 @@
|
|||
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.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.onCreateTag = function onCreateTag(tagName) {
|
||||
return $async(onCreateTagAsync, tagName);
|
||||
};
|
||||
|
||||
async function onCreateTagAsync(tagName) {
|
||||
try {
|
||||
const tag = await TagService.createTag(tagName);
|
||||
$scope.availableTags = $scope.availableTags.concat(tag);
|
||||
$scope.formValues.TagIds = $scope.formValues.TagIds.concat(tag.Id);
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to create tag');
|
||||
}
|
||||
}
|
||||
|
||||
$scope.createEndpointAndConfigureDevice = function () {
|
||||
return $async(async () => {
|
||||
$scope.state.actionInProgress = true;
|
||||
|
||||
let suffix = $scope.formValues.Suffix;
|
||||
|
||||
for (const deviceID of $scope.state.deviceIDs) {
|
||||
try {
|
||||
var endpoint = await EndpointService.createRemoteEndpoint(
|
||||
$scope.formValues.DeviceName + suffix,
|
||||
PortainerEndpointCreationTypes.EdgeAgentEnvironment,
|
||||
$scope.formValues.PortainerURL,
|
||||
'',
|
||||
$scope.formValues.GroupId,
|
||||
$scope.formValues.TagIds,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true
|
||||
);
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to create the environment');
|
||||
return;
|
||||
}
|
||||
|
||||
suffix++;
|
||||
|
||||
const config = {
|
||||
edgeID: endpoint.EdgeID,
|
||||
edgeKey: endpoint.EdgeKey,
|
||||
name: $scope.formValues.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('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(),
|
||||
tags: TagService.tags(),
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.groups = data.groups;
|
||||
$scope.availableTags = data.tags;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to load groups');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}
|
||||
);
|
73
app/portainer/views/devices/profiles/add/addProfile.html
Normal file
73
app/portainer/views/devices/profiles/add/addProfile.html
Normal file
|
@ -0,0 +1,73 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Create profile"></rd-header-title>
|
||||
<rd-header-content> <a ui-sref="portainer.settings">Settings</a> > Edge Compute </rd-header-content>
|
||||
</rd-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 />
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- build-method -->
|
||||
<div class="col-sm-12 form-section-title"> Profile configuration </div>
|
||||
<div class="form-group"></div>
|
||||
<div class="form-group" style="margin-bottom: 0">
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<input type="radio" id="method_editor" ng-model="state.method" value="editor" />
|
||||
<label for="method_editor">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
Web editor
|
||||
</div>
|
||||
<p>Use our Web editor</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !build-method -->
|
||||
|
||||
<web-editor-form
|
||||
ng-if="state.method === 'editor'"
|
||||
identifier="profile-creation-editor"
|
||||
value="formValues.profileFileContent"
|
||||
on-change="(onChangeFileContent)"
|
||||
ng-required="true"
|
||||
placeholder="#"
|
||||
>
|
||||
</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>
|
|
@ -0,0 +1,66 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { createProfile } from 'Portainer/hostmanagement/fdo/fdo.service';
|
||||
|
||||
angular.module('portainer.app').controller('AddProfileController', AddProfileController);
|
||||
|
||||
export default function AddProfileController($scope, $async, $state, $window, ModalService, Authentication, Notifications) {
|
||||
$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('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;
|
||||
}
|
||||
}
|
8
app/portainer/views/devices/profiles/add/index.js
Normal file
8
app/portainer/views/devices/profiles/add/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import controller from './addProfileController';
|
||||
|
||||
angular.module('portainer.app').component('addProfileView', {
|
||||
templateUrl: './addProfile.html',
|
||||
controller,
|
||||
});
|
73
app/portainer/views/devices/profiles/edit/editProfile.html
Normal file
73
app/portainer/views/devices/profiles/edit/editProfile.html
Normal file
|
@ -0,0 +1,73 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Edit profile"></rd-header-title>
|
||||
<rd-header-content> <a ui-sref="portainer.settings">Settings</a> > Edge Compute </rd-header-content>
|
||||
</rd-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 />
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- build-method -->
|
||||
<div class="col-sm-12 form-section-title"> Profile configuration </div>
|
||||
<div class="form-group"></div>
|
||||
<div class="form-group" style="margin-bottom: 0">
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<input type="radio" id="method_editor" ng-model="state.method" value="editor" />
|
||||
<label for="method_editor">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
Web editor
|
||||
</div>
|
||||
<p>Use our Web editor</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !build-method -->
|
||||
|
||||
<web-editor-form
|
||||
ng-if="state.method === 'editor'"
|
||||
identifier="profile-creation-editor"
|
||||
value="formValues.profileFileContent"
|
||||
on-change="(onChangeFileContent)"
|
||||
ng-required="true"
|
||||
placeholder="#"
|
||||
>
|
||||
</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>
|
|
@ -0,0 +1,84 @@
|
|||
import angular from 'angular';
|
||||
import { getProfile, updateProfile } from 'Portainer/hostmanagement/fdo/fdo.service';
|
||||
|
||||
angular.module('portainer.app').controller('EditProfileController', EditProfileController);
|
||||
|
||||
export default function EditProfileController($scope, $async, $state, $window, ModalService, Authentication, Notifications) {
|
||||
$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('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();
|
||||
}
|
8
app/portainer/views/devices/profiles/edit/index.js
Normal file
8
app/portainer/views/devices/profiles/edit/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import controller from './editProfileController';
|
||||
|
||||
angular.module('portainer.app').component('editProfileView', {
|
||||
templateUrl: './editProfile.html',
|
||||
controller,
|
||||
});
|
|
@ -24,11 +24,12 @@ angular
|
|||
StateManager
|
||||
) {
|
||||
$scope.state = {
|
||||
EnvironmentType: 'agent',
|
||||
EnvironmentType: $state.params.isEdgeDevice ? 'edge_agent' : 'agent',
|
||||
PlatformType: 'linux',
|
||||
actionInProgress: false,
|
||||
deploymentTab: 0,
|
||||
allowCreateTag: Authentication.isAdmin(),
|
||||
isEdgeDevice: $state.params.isEdgeDevice,
|
||||
availableEdgeAgentCheckinOptions: [
|
||||
{ key: 'Use default interval', value: 0 },
|
||||
{
|
||||
|
@ -271,7 +272,8 @@ angular
|
|||
TLSCAFile,
|
||||
TLSCertFile,
|
||||
TLSKeyFile,
|
||||
CheckinInterval
|
||||
CheckinInterval,
|
||||
$scope.state.isEdgeDevice
|
||||
);
|
||||
|
||||
Notifications.success('Environment created', name);
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<div class="form-group"></div>
|
||||
<div class="form-group" style="margin-bottom: 0">
|
||||
<div class="boxselector_wrapper">
|
||||
<div ng-click="resetEndpointURL()">
|
||||
<div ng-show="!state.isEdgeDevice" ng-click="resetEndpointURL()">
|
||||
<input type="radio" id="agent_endpoint" ng-model="state.EnvironmentType" value="agent" />
|
||||
<label for="agent_endpoint" data-cy="endpointCreate-agentSelectButton">
|
||||
<div class="boxselector_header">
|
||||
|
@ -32,7 +32,7 @@
|
|||
<p>Portainer Edge agent</p>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-click="resetEndpointURL()">
|
||||
<div ng-show="!state.isEdgeDevice" ng-click="resetEndpointURL()">
|
||||
<input type="radio" id="docker_endpoint" ng-model="state.EnvironmentType" value="docker" />
|
||||
<label for="docker_endpoint" data-cy="endpointCreate-dockerSelectButton">
|
||||
<div class="boxselector_header">
|
||||
|
@ -42,7 +42,7 @@
|
|||
<p>Directly connect to the Docker API</p>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-click="resetEndpointURL()">
|
||||
<div ng-show="!state.isEdgeDevice" ng-click="resetEndpointURL()">
|
||||
<input type="radio" id="kubernetes_endpoint" ng-model="state.EnvironmentType" value="kubernetes" />
|
||||
<label for="kubernetes_endpoint" data-cy="endpointCreate-kubeSelectButton">
|
||||
<div class="boxselector_header">
|
||||
|
@ -52,7 +52,7 @@
|
|||
<p>Local Kubernetes environment</p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-show="!state.isEdgeDevice">
|
||||
<input type="radio" id="azure_endpoint" ng-model="state.EnvironmentType" value="azure" />
|
||||
<label for="azure_endpoint" data-cy="endpointCreate-azureSelectButton">
|
||||
<div class="boxselector_header">
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<information-panel ng-if="state.edgeEndpoint && endpoint.EdgeID" title-text="Edge information">
|
||||
<information-panel ng-if="state.edgeEndpoint && endpoint.EdgeID && endpoint.LastCheckInDate" title-text="Edge information">
|
||||
<span class="small text-muted">
|
||||
<p>
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
|
@ -38,7 +38,7 @@
|
|||
</p>
|
||||
</span>
|
||||
</information-panel>
|
||||
<information-panel ng-if="state.edgeEndpoint && !endpoint.EdgeID" title-text="Deploy an agent">
|
||||
<information-panel ng-if="state.edgeEndpoint && !endpoint.LastCheckInDate" title-text="Deploy an agent">
|
||||
<span class="small text-muted">
|
||||
<p>
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
|
@ -65,17 +65,17 @@
|
|||
<uib-tabset active="state.deploymentTab">
|
||||
<uib-tab index="'kubernetes'" heading="Kubernetes" ng-if="state.platformType === 'linux'">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 45px">
|
||||
{{ dockerCommands[state.deploymentTab][state.platformType](agentVersion, agentShortVersion, randomEdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts) }}
|
||||
{{ dockerCommands[state.deploymentTab][state.platformType](agentVersion, agentShortVersion, endpoint.EdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts) }}
|
||||
</code>
|
||||
</uib-tab>
|
||||
<uib-tab index="'swarm'" heading="Docker Swarm">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 45px">
|
||||
{{ dockerCommands[state.deploymentTab][state.platformType](agentVersion, agentShortVersion, randomEdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts) }}
|
||||
{{ dockerCommands[state.deploymentTab][state.platformType](agentVersion, agentShortVersion, endpoint.EdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts) }}
|
||||
</code>
|
||||
</uib-tab>
|
||||
<uib-tab index="'standalone'" heading="Docker Standalone">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 45px">
|
||||
{{ dockerCommands[state.deploymentTab][state.platformType](agentVersion, agentShortVersion, randomEdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts) }}
|
||||
{{ dockerCommands[state.deploymentTab][state.platformType](agentVersion, agentShortVersion, endpoint.EdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts) }}
|
||||
</code>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
|
@ -204,6 +204,67 @@
|
|||
<por-endpoint-security form-data="formValues.SecurityFormData" endpoint="endpoint"></por-endpoint-security>
|
||||
</div>
|
||||
<!-- !endpoint-security -->
|
||||
<!-- 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..." />
|
||||
</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..." />
|
||||
</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"
|
||||
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"
|
||||
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"
|
||||
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
|
||||
|
|
|
@ -4,6 +4,8 @@ import uuidv4 from 'uuid/v4';
|
|||
import { PortainerEndpointTypes } from '@/portainer/models/endpoint/models';
|
||||
import { EndpointSecurityFormData } from '@/portainer/components/endpointSecurity/porEndpointSecurityModel';
|
||||
import { getAgentShortVersion } from 'Portainer/views/endpoints/helpers';
|
||||
import EndpointHelper from '@/portainer/helpers/endpointHelper';
|
||||
import { getAMTInfo } from 'Portainer/hostmanagement/open-amt/open-amt.service';
|
||||
|
||||
angular.module('portainer.app').controller('EndpointController', EndpointController);
|
||||
|
||||
|
@ -65,6 +67,7 @@ function EndpointController(
|
|||
{ key: '1 day', value: 86400 },
|
||||
],
|
||||
allowSelfSignedCerts: true,
|
||||
showAMTInfo: false,
|
||||
};
|
||||
|
||||
$scope.agentVersion = StateManager.getState().application.version;
|
||||
|
@ -94,7 +97,7 @@ function EndpointController(
|
|||
const command = $scope.dockerCommands[$scope.state.deploymentTab][$scope.state.platformType](
|
||||
$scope.agentVersion,
|
||||
$scope.agentShortVersion,
|
||||
$scope.randomEdgeID,
|
||||
$scope.endpoint.EdgeID,
|
||||
$scope.endpoint.EdgeKey,
|
||||
$scope.state.allowSelfSignedCerts
|
||||
);
|
||||
|
@ -268,7 +271,7 @@ function EndpointController(
|
|||
|
||||
if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
|
||||
$scope.edgeKeyDetails = decodeEdgeKey(endpoint.EdgeKey);
|
||||
$scope.randomEdgeID = uuidv4();
|
||||
endpoint.EdgeID = endpoint.EdgeID || uuidv4();
|
||||
|
||||
$scope.state.availableEdgeAgentCheckinOptions[0].key += ` (${settings.EdgeAgentCheckinInterval} seconds)`;
|
||||
}
|
||||
|
@ -278,12 +281,41 @@ function EndpointController(
|
|||
$scope.availableTags = tags;
|
||||
|
||||
configureState();
|
||||
|
||||
const disconnectedEdge = $scope.state.edgeEndpoint && !endpoint.EdgeID;
|
||||
if (EndpointHelper.isDockerEndpoint(endpoint) && !disconnectedEdge) {
|
||||
$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) {
|
||||
console.log('Failure', err, 'Unable to JSON parse AMT info: ' + amtInfo.RawOutput);
|
||||
clearAMTManagementInfo(amtInfo.RawOutput);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Failure', 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'] = '-';
|
||||
}
|
||||
|
||||
function buildLinuxStandaloneCommand(agentVersion, agentShortVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
return `
|
||||
docker run -d \\
|
||||
|
|
|
@ -7,7 +7,24 @@
|
|||
<rd-header-content>Environment management</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div
|
||||
class="row"
|
||||
style="width: 100%; height: 100%; text-align: center; display: flex; flex-direction: column; align-items: center; justify-content: center"
|
||||
ng-if="state.loadingMessage"
|
||||
>
|
||||
<div class="sk-fold">
|
||||
<div class="sk-fold-cube"></div>
|
||||
<div class="sk-fold-cube"></div>
|
||||
<div class="sk-fold-cube"></div>
|
||||
<div class="sk-fold-cube"></div>
|
||||
</div>
|
||||
<span style="margin-top: 25px">
|
||||
{{ state.loadingMessage }}
|
||||
<i class="fa fa-cog fa-spin"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="!state.loadingMessage">
|
||||
<div class="col-sm-12">
|
||||
<endpoints-datatable
|
||||
title-text="Environments"
|
||||
|
@ -16,6 +33,7 @@
|
|||
order-by="Name"
|
||||
remove-action="removeAction"
|
||||
retrieve-page="getPaginatedEndpoints"
|
||||
set-loading-message="setLoadingMessage"
|
||||
></endpoints-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,8 +4,16 @@ import EndpointHelper from 'Portainer/helpers/endpointHelper';
|
|||
angular.module('portainer.app').controller('EndpointsController', EndpointsController);
|
||||
|
||||
function EndpointsController($q, $scope, $state, $async, EndpointService, GroupService, ModalService, Notifications, EndpointProvider, StateManager) {
|
||||
$scope.removeAction = removeAction;
|
||||
$scope.state = {
|
||||
loadingMessage: '',
|
||||
};
|
||||
|
||||
$scope.setLoadingMessage = setLoadingMessage;
|
||||
function setLoadingMessage(message) {
|
||||
$scope.state.loadingMessage = message;
|
||||
}
|
||||
|
||||
$scope.removeAction = removeAction;
|
||||
function removeAction(endpoints) {
|
||||
ModalService.confirmDeletion('This action will remove all configurations associated to your environment(s). Continue?', (confirmed) => {
|
||||
if (!confirmed) {
|
||||
|
|
19
app/portainer/views/endpoints/kvm/KVMControl.css
Normal file
19
app/portainer/views/endpoints/kvm/KVMControl.css
Normal file
|
@ -0,0 +1,19 @@
|
|||
.canvas-container .header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.kvm-maximized {
|
||||
position: fixed;
|
||||
background: white;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
z-index: 1000;
|
||||
max-height: 100% !important;
|
||||
overflow-y: scroll;
|
||||
}
|
45
app/portainer/views/endpoints/kvm/KVMControl.tsx
Normal file
45
app/portainer/views/endpoints/kvm/KVMControl.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { useEffect, createRef } from 'react';
|
||||
import { KVM } from '@open-amt-cloud-toolkit/ui-toolkit-react/reactjs/src/kvm.bundle';
|
||||
|
||||
import { react2angular } from '@/react-tools/react2angular';
|
||||
|
||||
import './KVMControl.css';
|
||||
|
||||
export interface KVMControlProps {
|
||||
deviceId: string;
|
||||
server: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export function KVMControl({ deviceId, server, token }: KVMControlProps) {
|
||||
const divRef = createRef<HTMLInputElement>();
|
||||
useEffect(() => {
|
||||
if (divRef.current) {
|
||||
const connectButton = divRef.current.querySelector('button');
|
||||
if (connectButton) {
|
||||
connectButton.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!deviceId || !server || !token) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<div ref={divRef}>
|
||||
<KVM
|
||||
deviceId={deviceId}
|
||||
mpsServer={`https://${server}/mps/ws/relay`}
|
||||
authToken={token}
|
||||
mouseDebounceTime="200"
|
||||
canvasHeight="100%"
|
||||
canvasWidth="100%"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const KVMControlAngular = react2angular(KVMControl, [
|
||||
'deviceId',
|
||||
'server',
|
||||
'token',
|
||||
]);
|
27
app/portainer/views/endpoints/kvm/endpointKVM.html
Normal file
27
app/portainer/views/endpoints/kvm/endpointKVM.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="KVM Control"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.endpoints">Environments</a> > <a ui-sref="portainer.endpoints.endpoint({id: $state.endpoint.Id})">{{ $state.endpoint.Name }}</a> >
|
||||
{{ $state.deviceName }} > KVM Control
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="datatable" ng-class="{ 'kvm-maximized': $state.maximized }">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle">KVM Control</div>
|
||||
<i class="interactive fa fa-times fa-lg pull-right" aria-hidden="true" ui-sref="edge.devices"></i>
|
||||
<i ng-click="maximize()" title="Maximize" class="interactive fa fa-window-maximize fa-lg pull-right" style="margin-right: 10px" aria-hidden="true"></i>
|
||||
<i ng-click="minimize()" title="Minimize" class="interactive fa fa-window-minimize fa-lg pull-right" style="margin-right: 10px" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<kvm-control device-id="$state.deviceId" server="$state.mpsServer" token="$state.mpsToken"> </kvm-control>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
57
app/portainer/views/endpoints/kvm/endpointKVMController.js
Normal file
57
app/portainer/views/endpoints/kvm/endpointKVMController.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { enableDeviceFeatures } from 'Portainer/hostmanagement/open-amt/open-amt.service';
|
||||
|
||||
class EndpointKVMController {
|
||||
/* @ngInject */
|
||||
constructor($state, $scope, $async, $transition$, EndpointService, Notifications) {
|
||||
this.$state = $state;
|
||||
this.$scope = $scope;
|
||||
this.$async = $async;
|
||||
this.$transition$ = $transition$;
|
||||
this.Notifications = Notifications;
|
||||
this.EndpointService = EndpointService;
|
||||
|
||||
this.$state.maximized = false;
|
||||
this.$state.endpointId = $transition$.params().id;
|
||||
this.$state.deviceId = $transition$.params().deviceId;
|
||||
this.$state.deviceName = $transition$.params().deviceName;
|
||||
|
||||
$scope.maximize = function () {
|
||||
this.$state.maximized = true;
|
||||
};
|
||||
|
||||
$scope.minimize = function () {
|
||||
this.$state.maximized = false;
|
||||
};
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
this.$async(async () => {
|
||||
try {
|
||||
this.$state.endpoint = await this.EndpointService.endpoint(this.$state.endpointId);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve environment information');
|
||||
}
|
||||
|
||||
try {
|
||||
const features = {
|
||||
ider: true,
|
||||
kvm: true,
|
||||
sol: true,
|
||||
redirection: true,
|
||||
userConsent: 'none',
|
||||
};
|
||||
const mpsAuthorization = await enableDeviceFeatures(this.$state.endpointId, this.$state.deviceId, features);
|
||||
|
||||
this.$state.mpsServer = mpsAuthorization.Server;
|
||||
this.$state.mpsToken = mpsAuthorization.Token;
|
||||
} catch (e) {
|
||||
this.Notifications.error('Failure', e, `Failed to load kvm for device`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default EndpointKVMController;
|
||||
angular.module('portainer.app').controller('EndpointKVMController', EndpointKVMController);
|
5
app/portainer/views/endpoints/kvm/index.js
Normal file
5
app/portainer/views/endpoints/kvm/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { KVMControlAngular } from '@/portainer/views/endpoints/kvm/KVMControl';
|
||||
|
||||
angular.module('portainer.app').component('kvmControl', KVMControlAngular).name;
|
|
@ -1,4 +1,5 @@
|
|||
import EndpointHelper from 'Portainer/helpers/endpointHelper';
|
||||
import { getEndpoints } from 'Portainer/environments/environment.service';
|
||||
|
||||
angular
|
||||
.module('portainer.app')
|
||||
|
@ -65,7 +66,7 @@ angular
|
|||
function getPaginatedEndpoints(lastId, limit, search) {
|
||||
const deferred = $q.defer();
|
||||
$q.all({
|
||||
endpoints: EndpointService.endpoints(lastId, limit, { search }),
|
||||
endpoints: getEndpoints(lastId, limit, { search, edgeDeviceFilter: false }),
|
||||
groups: GroupService.groups(),
|
||||
})
|
||||
.then(function success(data) {
|
||||
|
|
7
app/portainer/views/settings/edge-compute/index.js
Normal file
7
app/portainer/views/settings/edge-compute/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import angular from 'angular';
|
||||
import controller from './settingsEdgeComputeController';
|
||||
|
||||
angular.module('portainer.app').component('settingsEdgeComputeView', {
|
||||
templateUrl: './settingsEdgeCompute.html',
|
||||
controller,
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Settings"></rd-header-title>
|
||||
<rd-header-content> <a ui-sref="portainer.settings">Settings</a> > Edge Compute </rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<settings-edge-compute on-submit="($ctrl.onSubmitEdgeCompute)" settings="($ctrl.settings)"></settings-edge-compute>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<settings-open-amt on-submit="($ctrl.onSubmitOpenAMT)" settings="($ctrl.settings)"></settings-open-amt>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<settings-fdo on-submit="($ctrl.onSubmitFDO)" settings="($ctrl.settings)"></settings-fdo>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,54 @@
|
|||
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);
|
||||
|
||||
/* @ngInject */
|
||||
export default function SettingsEdgeComputeController($q, $async, $state, Notifications, SettingsService, StateManager) {
|
||||
var ctrl = this;
|
||||
|
||||
this.onSubmitEdgeCompute = async function (settings) {
|
||||
try {
|
||||
await SettingsService.update(settings);
|
||||
Notifications.success('Settings updated');
|
||||
StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures);
|
||||
$state.reload();
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to update settings');
|
||||
}
|
||||
};
|
||||
|
||||
this.onSubmitOpenAMT = async function (formValues) {
|
||||
try {
|
||||
await configureAMT(formValues);
|
||||
Notifications.success(`OpenAMT successfully ${formValues.enabled ? 'enabled' : 'disabled'}`);
|
||||
$state.reload();
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Failed applying changes');
|
||||
}
|
||||
};
|
||||
|
||||
this.onSubmitFDO = async function (formValues) {
|
||||
try {
|
||||
await configureFDO(formValues);
|
||||
Notifications.success(`FDO successfully ${formValues.enabled ? 'enabled' : 'disabled'}`);
|
||||
$state.reload();
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Failed applying changes');
|
||||
}
|
||||
};
|
||||
|
||||
function initView() {
|
||||
$async(async () => {
|
||||
try {
|
||||
ctrl.settings = await SettingsService.settings();
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve application settings');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}
|
|
@ -91,40 +91,10 @@
|
|||
</div>
|
||||
<!-- !helm charts -->
|
||||
<!-- host-filesystem -->
|
||||
|
||||
<!-- edge -->
|
||||
<div class="col-sm-12 form-section-title"> Edge Compute </div>
|
||||
<div class="form-group">
|
||||
<label for="edge_checkin" class="col-sm-2 control-label text-left">
|
||||
Edge agent default poll frequency
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="Interval used by default by each Edge agent to check in with the Portainer instance. Affects Edge environment management and Edge compute features."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select
|
||||
id="edge_checkin"
|
||||
class="form-control"
|
||||
ng-model="settings.EdgeAgentCheckinInterval"
|
||||
ng-options="+(opt.value) as opt.key for opt in state.availableEdgeAgentCheckinOptions"
|
||||
data-cy="settings-pollFrequencySelect"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_enableEdgeComputeFeatures" class="control-label text-left"> Enable edge compute features </label>
|
||||
<label class="switch" style="margin-left: 20px" data-cy="settings-edgeToggle">
|
||||
<input type="checkbox" name="toggle_enableEdgeComputeFeatures" ng-model="formValues.enableEdgeComputeFeatures" /><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !edge -->
|
||||
<!-- kube -->
|
||||
<div class="col-sm-12 form-section-title"> Kubernetes </div>
|
||||
<div class="form-group">
|
||||
<label for="edge_checkin" class="col-sm-2 control-label text-left"> Kubeconfig expiry </label>
|
||||
<label for="kubeconfig_expiry" class="col-sm-2 control-label text-left"> Kubeconfig expiry </label>
|
||||
<div class="col-sm-10">
|
||||
<select
|
||||
id="kubeconfig_expiry"
|
||||
|
@ -160,8 +130,6 @@
|
|||
|
||||
<ssl-certificate-settings></ssl-certificate-settings>
|
||||
|
||||
<open-amt-settings ng-if="settings.FeatureFlagSettings && settings.FeatureFlagSettings['open-amt']"></open-amt-settings>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
|
|
|
@ -21,20 +21,6 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||
|
||||
$scope.state = {
|
||||
actionInProgress: false,
|
||||
availableEdgeAgentCheckinOptions: [
|
||||
{
|
||||
key: '5 seconds',
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
key: '10 seconds',
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
key: '30 seconds',
|
||||
value: 30,
|
||||
},
|
||||
],
|
||||
availableKubeconfigExpiryOptions: [
|
||||
{
|
||||
key: '1 day',
|
||||
|
@ -67,7 +53,6 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||
customLogo: false,
|
||||
labelName: '',
|
||||
labelValue: '',
|
||||
enableEdgeComputeFeatures: false,
|
||||
enableTelemetry: false,
|
||||
passwordProtect: false,
|
||||
password: '',
|
||||
|
@ -132,7 +117,6 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||
settings.LogoURL = '';
|
||||
}
|
||||
|
||||
settings.EnableEdgeComputeFeatures = $scope.formValues.enableEdgeComputeFeatures;
|
||||
settings.EnableTelemetry = $scope.formValues.enableTelemetry;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
|
@ -145,7 +129,6 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||
Notifications.success('Settings updated');
|
||||
StateManager.updateLogo(settings.LogoURL);
|
||||
StateManager.updateSnapshotInterval(settings.SnapshotInterval);
|
||||
StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures);
|
||||
StateManager.updateEnableTelemetry(settings.EnableTelemetry);
|
||||
$state.reload();
|
||||
})
|
||||
|
@ -166,7 +149,6 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||
if (settings.LogoURL !== '') {
|
||||
$scope.formValues.customLogo = true;
|
||||
}
|
||||
$scope.formValues.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
||||
$scope.formValues.enableTelemetry = settings.EnableTelemetry;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
|
|
@ -60,6 +60,9 @@
|
|||
</sidebar-section>
|
||||
|
||||
<sidebar-section title="Edge compute" ng-if="isAdmin && applicationState.application.enableEdgeComputeFeatures">
|
||||
<sidebar-menu-item path="edge.devices" icon-class="fas fa-laptop-code fa-fw" class-name="sidebar-list" data-cy="portainerSidebar-edgeDevices">
|
||||
Edge Devices
|
||||
</sidebar-menu-item>
|
||||
<sidebar-menu-item path="edge.groups" icon-class="fa-object-group fa-fw" class-name="sidebar-list" data-cy="portainerSidebar-edgeGroups">Edge Groups</sidebar-menu-item>
|
||||
<sidebar-menu-item path="edge.stacks" icon-class="fa-layer-group fa-fw" class-name="sidebar-list" data-cy="portainerSidebar-edgeStacks">Edge Stacks</sidebar-menu-item>
|
||||
<sidebar-menu-item path="edge.jobs" icon-class="fa-clock fa-fw" class-name="sidebar-list" data-cy="portainerSidebar-edgeJobs">Edge Jobs</sidebar-menu-item>
|
||||
|
@ -97,8 +100,15 @@
|
|||
<sidebar-menu-item path="portainer.activityLogs" class-name="sidebar-sublist" data-cy="portainerSidebar-activityLogs">Activity Logs</sidebar-menu-item>
|
||||
</sidebar-menu>
|
||||
|
||||
<sidebar-menu label="Settings" icon-class="fa-cogs fa-fw" path="portainer.settings" is-sidebar-open="toggle" children-paths="['portainer.settings.authentication']">
|
||||
<sidebar-menu
|
||||
label="Settings"
|
||||
icon-class="fa-cogs fa-fw"
|
||||
path="portainer.settings"
|
||||
is-sidebar-open="toggle"
|
||||
children-paths="['portainer.settings.authentication', 'portainer.settings.edgeCompute']"
|
||||
>
|
||||
<sidebar-menu-item path="portainer.settings.authentication" class-name="sidebar-sublist" data-cy="portainerSidebar-authentication">Authentication</sidebar-menu-item>
|
||||
<sidebar-menu-item path="portainer.settings.edgeCompute" class-name="sidebar-sublist" data-cy="portainerSidebar-edge-compute">Edge Compute</sidebar-menu-item>
|
||||
|
||||
<div class="sidebar-sublist">
|
||||
<a href="https://www.portainer.io/community_help" target="_blank" data-cy="portainerSidebar-help">Help / About</a>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue