1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-10 00:05:24 +02:00

feat(environments): create async edge [EE-4480] (#8527)

This commit is contained in:
Chaim Lev-Ari 2023-03-01 20:33:05 +02:00 committed by GitHub
parent bc6a667a6b
commit c819d4e7f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 880 additions and 586 deletions

View file

@ -1,5 +1,3 @@
import { Globe } from 'lucide-react';
import { Environment } from '@/react/portainer/environments/types';
import { isAgentEnvironment } from '@/react/portainer/environments/utils';
@ -8,15 +6,5 @@ export function AgentDetails({ environment }: { environment: Environment }) {
return null;
}
return (
<>
<span>{environment.Agent.Version}</span>
{environment.Edge.AsyncMode && (
<span className="vertical-center gap-1">
<Globe className="icon icon-sm space-right" aria-hidden="true" />
Async Environment
</span>
)}
</>
);
return <span>{environment.Agent.Version}</span>;
}

View file

@ -31,12 +31,12 @@ export function EnvironmentTypeTag({
}
function getTypeLabel(environment: Environment) {
if (environment.IsEdgeDevice) {
return 'Edge Device';
}
if (isEdgeEnvironment(environment.Type)) {
return 'Edge Agent';
if (environment.Edge.AsyncMode) {
return 'Edge Agent Async';
}
return 'Edge Agent Standard';
}
if (isLocalEnvironment(environment)) {

View file

@ -110,6 +110,7 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
tagsPartialMatch: true,
agentVersions: agentVersions.map((a) => a.value),
updateInformation: isBE,
edgeAsync: getEdgeAsyncValue(connectionTypes),
};
const queryWithSort = {
@ -282,8 +283,8 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
EnvironmentType.AgentOnDocker,
EnvironmentType.AgentOnKubernetes,
],
[ConnectionType.EdgeAgent]: EdgeTypes,
[ConnectionType.EdgeDevice]: EdgeTypes,
[ConnectionType.EdgeAgentStandard]: EdgeTypes,
[ConnectionType.EdgeAgentAsync]: EdgeTypes,
};
const selectedTypesByPlatform = platformTypes.flatMap(
@ -405,3 +406,21 @@ function renderItems(
return items;
}
function getEdgeAsyncValue(connectionTypes: Filter<ConnectionType>[]) {
const hasEdgeAsync = connectionTypes.some(
(connectionType) => connectionType.value === ConnectionType.EdgeAgentAsync
);
const hasEdgeStandard = connectionTypes.some(
(connectionType) =>
connectionType.value === ConnectionType.EdgeAgentStandard
);
// If both are selected, we don't want to filter on either, and same for if both are not selected
if (hasEdgeAsync === hasEdgeStandard) {
return undefined;
}
return hasEdgeAsync;
}

View file

@ -176,22 +176,26 @@ function getConnectionTypeOptions(platformTypes: Filter<PlatformType>[]) {
[PlatformType.Docker]: [
ConnectionType.API,
ConnectionType.Agent,
ConnectionType.EdgeAgent,
ConnectionType.EdgeDevice,
ConnectionType.EdgeAgentStandard,
ConnectionType.EdgeAgentAsync,
],
[PlatformType.Azure]: [ConnectionType.API],
[PlatformType.Kubernetes]: [
ConnectionType.Agent,
ConnectionType.EdgeAgent,
ConnectionType.EdgeDevice,
ConnectionType.EdgeAgentStandard,
ConnectionType.EdgeAgentAsync,
],
[PlatformType.Nomad]: [
ConnectionType.EdgeAgentStandard,
ConnectionType.EdgeAgentAsync,
],
[PlatformType.Nomad]: [ConnectionType.EdgeAgent, ConnectionType.EdgeDevice],
};
const connectionTypesDefaultOptions = [
{ value: ConnectionType.API, label: 'API' },
{ value: ConnectionType.Agent, label: 'Agent' },
{ value: ConnectionType.EdgeAgent, label: 'Edge Agent' },
{ value: ConnectionType.EdgeAgentStandard, label: 'Edge Agent Standard' },
{ value: ConnectionType.EdgeAgentAsync, label: 'Edge Agent Async' },
];
if (platformTypes.length === 0) {
@ -226,12 +230,12 @@ function getPlatformTypeOptions(connectionTypes: Filter<ConnectionType>[]) {
const connectionTypePlatformType = {
[ConnectionType.API]: [PlatformType.Docker, PlatformType.Azure],
[ConnectionType.Agent]: [PlatformType.Docker, PlatformType.Kubernetes],
[ConnectionType.EdgeAgent]: [
[ConnectionType.EdgeAgentStandard]: [
PlatformType.Kubernetes,
PlatformType.Nomad,
PlatformType.Docker,
],
[ConnectionType.EdgeDevice]: [
[ConnectionType.EdgeAgentAsync]: [
PlatformType.Nomad,
PlatformType.Docker,
PlatformType.Kubernetes,

View file

@ -6,6 +6,6 @@ export interface Filter<T = number> {
export enum ConnectionType {
API,
Agent,
EdgeAgent,
EdgeDevice,
EdgeAgentStandard,
EdgeAgentAsync,
}

View file

@ -0,0 +1,65 @@
import { Field, useField } from 'formik';
import { string } from 'yup';
import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
interface Props {
fieldName: string;
readonly?: boolean;
required?: boolean;
}
export function PortainerTunnelAddrField({
fieldName,
readonly,
required,
}: Props) {
const [, metaProps] = useField(fieldName);
const id = `${fieldName}-input`;
return (
<FormControl
label="Portainer tunnel server address"
tooltip="Address of this Portainer instance that will be used by Edge agents to establish a reverse tunnel."
required
errors={metaProps.error}
inputId={id}
>
<Field
id={id}
name={fieldName}
as={Input}
placeholder="portainer.mydomain.tld"
required={required}
readOnly={readonly}
/>
</FormControl>
);
}
export function validation() {
return string()
.required('Tunnel server address is required')
.test(
'valid tunnel server URL',
'The tunnel server address must be a valid address (localhost cannot be used)',
(value) => {
if (!value) {
return false;
}
return !value.startsWith('localhost');
}
);
}
/**
* Returns an address that can be used as a default value for the Portainer tunnel server address
* based on the current window location.
* Used for Edge Compute.
*
*/
export function buildDefaultValue() {
return `${window.location.hostname}:8000`;
}

View file

@ -7,36 +7,23 @@ import { Input } from '@@/form-components/Input';
interface Props {
fieldName: string;
readonly?: boolean;
required?: boolean;
tooltip?: string;
}
export function validation() {
return string()
.test(
'url',
'URL should be a valid URI and cannot include localhost',
(value) => {
if (!value) {
return false;
}
try {
const url = new URL(value);
return url.hostname !== 'localhost';
} catch {
return false;
}
}
)
.required('URL is required');
}
export function PortainerUrlField({ fieldName, readonly }: Props) {
export function PortainerUrlField({
fieldName,
readonly,
required,
tooltip = 'URL of the Portainer instance that the agent will use to initiate the communications.',
}: Props) {
const [, metaProps] = useField(fieldName);
const id = `${fieldName}-input`;
return (
<FormControl
label="Portainer server URL"
tooltip="URL of the Portainer instance that the agent will use to initiate the communications."
label="Portainer API server URL"
tooltip={tooltip}
required
errors={metaProps.error}
inputId={id}
@ -45,11 +32,42 @@ export function PortainerUrlField({ fieldName, readonly }: Props) {
id={id}
name={fieldName}
as={Input}
placeholder="e.g. https://10.0.0.10:9443 or https://portainer.mydomain.com"
required
placeholder="https://portainer.mydomain.tld"
required={required}
data-cy="endpointCreate-portainerServerUrlInput"
readOnly={readonly}
/>
</FormControl>
);
}
export function validation() {
return string()
.required('API server URL is required')
.test(
'valid API server URL',
'The API server URL must be a valid URL (localhost cannot be used)',
(value) => {
if (!value) {
return false;
}
try {
const url = new URL(value);
return !!url.hostname && url.hostname !== 'localhost';
} catch {
return false;
}
}
);
}
/**
* Returns a URL that can be used as a default value for the Portainer server API URL
* based on the current window location.
* Used for Edge Compute.
*
*/
export function buildDefaultValue() {
return `${window.location.protocol}//${window.location.host}`;
}

View file

@ -0,0 +1,180 @@
import { useMutation } from 'react-query';
import { useEffect, useState } from 'react';
import { Laptop } from 'lucide-react';
import { generateKey } from '@/react/portainer/environments/environment.service/edge';
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
import { useSettings } from '@/react/portainer/settings/queries';
import EdgeAgentStandardIcon from '@/react/edge/components/edge-agent-standard.svg?c';
import EdgeAgentAsyncIcon from '@/react/edge/components/edge-agent-async.svg?c';
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
import { TextTip } from '@@/Tip/TextTip';
import { BoxSelector } from '@@/BoxSelector';
import { FormSection } from '@@/form-components/FormSection';
import { CopyButton } from '@@/buttons';
import { Link } from '@@/Link';
import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
const commands = {
linux: [
commandsTabs.k8sLinux,
commandsTabs.swarmLinux,
commandsTabs.standaloneLinux,
commandsTabs.nomadLinux,
],
win: [commandsTabs.swarmWindows, commandsTabs.standaloneWindow],
};
const asyncModeOptions = [
{
icon: EdgeAgentStandardIcon,
id: 'standard',
label: 'Edge Agent Standard',
value: false,
iconType: 'badge',
},
{
icon: EdgeAgentAsyncIcon,
id: 'async',
label: 'Edge Agent Async',
value: true,
iconType: 'badge',
},
] as const;
export function AutomaticEdgeEnvCreation() {
const edgeKeyMutation = useGenerateKeyMutation();
const { mutate: generateKey, reset: resetKey } = edgeKeyMutation;
const settingsQuery = useSettings();
const [asyncMode, setAsyncMode] = useState(false);
const url = settingsQuery.data?.EdgePortainerUrl;
const settings = settingsQuery.data;
const edgeKey = edgeKeyMutation.data;
const edgeComputeConfigurationOK = validateConfiguration();
useEffect(() => {
if (edgeComputeConfigurationOK) {
generateKey();
} else {
resetKey();
}
}, [generateKey, edgeComputeConfigurationOK, resetKey]);
if (!settingsQuery.data) {
return null;
}
return (
<Widget>
<WidgetTitle icon={Laptop} title="Automatic Edge Environment Creation" />
<WidgetBody className="form-horizontal">
{!edgeComputeConfigurationOK ? (
<TextTip color="orange">
In order to use this feature, please turn on Edge Compute features{' '}
<Link to="portainer.settings.edgeCompute">here</Link> and have
Portainer API server URL and tunnel server address properly
configured.
</TextTip>
) : (
<>
<BoxSelector
slim
radioName="async-mode-selector"
value={asyncMode}
onChange={handleChangeAsyncMode}
options={asyncModeOptions}
/>
<EdgeKeyInfo
asyncMode={asyncMode}
edgeKey={edgeKey}
isLoading={edgeKeyMutation.isLoading}
url={url}
tunnelUrl={settings?.Edge.TunnelServerAddress}
/>
</>
)}
</WidgetBody>
</Widget>
);
function handleChangeAsyncMode(asyncMode: boolean) {
setAsyncMode(asyncMode);
}
function validateConfiguration() {
return !!(
settings &&
settings.EnableEdgeComputeFeatures &&
settings.EdgePortainerUrl &&
settings.Edge.TunnelServerAddress
);
}
}
// using mutation because we want this action to run only when required
function useGenerateKeyMutation() {
return useMutation(generateKey);
}
function EdgeKeyInfo({
isLoading,
edgeKey,
url,
tunnelUrl,
asyncMode,
}: {
isLoading: boolean;
edgeKey?: string;
url?: string;
tunnelUrl?: string;
asyncMode: boolean;
}) {
if (isLoading || !edgeKey) {
return <div>Generating key for {url} ... </div>;
}
return (
<>
<hr />
<FormSection title="Edge key">
<div className="break-words">
<code>{edgeKey}</code>
</div>
<CopyButton copyText={edgeKey}>Copy token</CopyButton>
</FormSection>
<hr />
<EdgeScriptForm
edgeInfo={{ key: edgeKey }}
commands={commands}
isNomadTokenVisible
asyncMode={asyncMode}
>
<FormControl label="Portainer API server URL">
<Input value={url} readOnly />
</FormControl>
{!asyncMode && (
<FormControl label="Portainer tunnel server address">
<Input value={tunnelUrl} readOnly />
</FormControl>
)}
<TextTip color="blue">
Portainer Server URL{' '}
{!asyncMode ? 'and tunnel server address are' : 'is'} set{' '}
<Link to="portainer.settings.edgeCompute">here</Link>
</TextTip>
</EdgeScriptForm>
</>
);
}

View file

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

View file

@ -0,0 +1,27 @@
import { withLimitToBE } from '@/react/hooks/useLimitToBE';
import { PageHeader } from '@@/PageHeader';
import { AutomaticEdgeEnvCreation } from './AutomaticEdgeEnvCreation';
export const EdgeAutoCreateScriptViewWrapper = withLimitToBE(
EdgeAutoCreateScriptView
);
function EdgeAutoCreateScriptView() {
return (
<>
<PageHeader
title="Automatic Edge Environment Creation"
breadcrumbs={[
{ label: 'Environments', link: 'portainer.endpoints' },
'Automatic Edge Environment Creation',
]}
/>
<div className="mx-3">
<AutomaticEdgeEnvCreation />
</div>
</>
);
}

View file

@ -0,0 +1 @@
export { EdgeAutoCreateScriptViewWrapper as EdgeAutoCreateScriptView } from './EdgeAutoCreateScriptView';

View file

@ -0,0 +1,28 @@
import { Plus } from 'lucide-react';
import { Button } from '@@/buttons';
import { Link } from '@@/Link';
import { useSettings } from '../../settings/queries';
export function ImportFdoDeviceButton() {
const isFDOEnabledQuery = useSettings(
(settings) => settings.fdoConfiguration.enabled
);
if (!isFDOEnabledQuery.data) {
return null;
}
return (
<Button
type="button"
color="secondary"
icon={Plus}
as={Link}
props={{ to: 'portainer.endpoints.importDevice' }}
>
Import FDO device
</Button>
);
}

View file

@ -2,6 +2,7 @@ import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationV
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { type EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
import { type TagId } from '@/portainer/tags/types';
import { EdgeAsyncIntervalsValues } from '@/react/edge/components/EdgeAsyncIntervalsForm';
import { type Environment, EnvironmentCreationTypes } from '../types';
@ -101,6 +102,10 @@ interface TLSSettings {
keyFile?: File;
}
interface EdgeSettings extends EdgeAsyncIntervalsValues {
asyncMode: boolean;
}
export interface EnvironmentOptions {
url?: string;
publicUrl?: string;
@ -110,6 +115,8 @@ export interface EnvironmentOptions {
isEdgeDevice?: boolean;
gpus?: Gpu[];
pollFrequency?: number;
edge?: EdgeSettings;
tunnelServerAddr?: string;
}
interface CreateRemoteEnvironment {
@ -163,10 +170,12 @@ export function createAgentEnvironment({
interface CreateEdgeAgentEnvironment {
name: string;
portainerUrl: string;
tunnelServerAddr?: string;
meta?: EnvironmentMetadata;
pollFrequency: number;
gpus?: Gpu[];
isEdgeDevice?: boolean;
edge: EdgeSettings;
}
export function createEdgeAgentEnvironment({
@ -176,6 +185,7 @@ export function createEdgeAgentEnvironment({
gpus = [],
isEdgeDevice,
pollFrequency,
edge,
}: CreateEdgeAgentEnvironment) {
return createEnvironment(
name,
@ -189,6 +199,7 @@ export function createEdgeAgentEnvironment({
gpus,
isEdgeDevice,
pollFrequency,
edge,
meta,
}
);
@ -240,6 +251,16 @@ async function createEnvironment(
AzureAuthenticationKey: azure.authenticationKey,
};
}
if (options.edge?.asyncMode) {
payload = {
...payload,
EdgeAsyncMode: true,
EdgePingInterval: options.edge?.PingInterval,
EdgeSnapshotInterval: options.edge?.SnapshotInterval,
EdgeCommandInterval: options.edge?.CommandInterval,
};
}
}
const formPayload = json2formData(payload);

View file

@ -22,7 +22,7 @@ export interface EnvironmentsQueryParams {
tagsPartialMatch?: boolean;
groupIds?: EnvironmentGroupId[];
status?: EnvironmentStatus[];
edgeDevice?: boolean;
edgeAsync?: boolean;
edgeDeviceUntrusted?: boolean;
excludeSnapshots?: boolean;
provisioned?: boolean;

View file

@ -15,6 +15,5 @@ export function useAgentDetails() {
return {
agentVersion,
agentSecret: settingsQuery.data.AgentSecret,
useEdgeAsyncMode: settingsQuery.data.Edge.AsyncMode,
};
}

View file

@ -205,12 +205,14 @@ function useAnalyticsState() {
dockerAgent: 0,
dockerApi: 0,
kubernetesAgent: 0,
kubernetesEdgeAgent: 0,
kubernetesEdgeAgentAsync: 0,
kubernetesEdgeAgentStandard: 0,
kaasAgent: 0,
aciApi: 0,
localEndpoint: 0,
nomadEdgeAgent: 0,
dockerEdgeAgent: 0,
nomadEdgeAgentStandard: 0,
dockerEdgeAgentAsync: 0,
dockerEdgeAgentStandard: 0,
});
return { analytics, setAnalytics };

View file

@ -1,7 +1,5 @@
import { useState } from 'react';
import { useAgentDetails } from '@/react/portainer/environments/queries/useAgentDetails';
import { CopyButton } from '@@/buttons/CopyButton';
import { Code } from '@@/Code';
import { NavTabs } from '@@/NavTabs';
@ -22,12 +20,6 @@ const deployments = [
export function DeploymentScripts() {
const [deployType, setDeployType] = useState(deployments[0].id);
const agentDetailsQuery = useAgentDetails();
if (!agentDetailsQuery) {
return null;
}
const options = deployments.map((c) => ({
id: c.id,
label: c.label,

View file

@ -1,8 +1,12 @@
import { useState } from 'react';
import { Zap, Cloud, Network, Plug2 } from 'lucide-react';
import { Zap, Network, Plug2 } from 'lucide-react';
import _ from 'lodash';
import { Environment } from '@/react/portainer/environments/types';
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import EdgeAgentStandardIcon from '@/react/edge/components/edge-agent-standard.svg?c';
import EdgeAgentAsyncIcon from '@/react/edge/components/edge-agent-async.svg?c';
import { BoxSelector, type BoxSelectorOption } from '@@/BoxSelector';
import { BadgeIcon } from '@@/BadgeIcon';
@ -21,8 +25,8 @@ interface Props {
}
const defaultOptions: BoxSelectorOption<
'agent' | 'api' | 'socket' | 'edgeAgent'
>[] = [
'agent' | 'api' | 'socket' | 'edgeAgentStandard' | 'edgeAgentAsync'
>[] = _.compact([
{
id: 'agent',
icon: <BadgeIcon icon={Zap} size="3xl" />,
@ -45,17 +49,28 @@ const defaultOptions: BoxSelectorOption<
value: 'socket',
},
{
id: 'edgeAgent',
icon: <BadgeIcon icon={Cloud} size="3xl" />,
label: 'Edge Agent',
id: 'edgeAgentStandard',
icon: EdgeAgentStandardIcon,
iconType: 'badge',
label: 'Edge Agent Standard',
description: '',
value: 'edgeAgent',
hide: window.ddExtension,
value: 'edgeAgentStandard',
},
];
isBE && {
id: 'edgeAgentAsync',
icon: EdgeAgentAsyncIcon,
iconType: 'badge',
label: 'Edge Agent Async',
description: '',
value: 'edgeAgentAsync',
},
]);
export function WizardDocker({ onCreate, isDockerStandalone }: Props) {
const options = useFilterEdgeOptionsIfNeeded(defaultOptions, 'edgeAgent');
const options = useFilterEdgeOptionsIfNeeded(
defaultOptions,
'edgeAgentStandard'
);
const [creationType, setCreationType] = useState(options[0].value);
@ -74,7 +89,14 @@ export function WizardDocker({ onCreate, isDockerStandalone }: Props) {
</div>
);
function getTab(creationType: 'agent' | 'api' | 'socket' | 'edgeAgent') {
function getTab(
creationType:
| 'agent'
| 'api'
| 'socket'
| 'edgeAgentStandard'
| 'edgeAgentAsync'
) {
switch (creationType) {
case 'agent':
return (
@ -95,10 +117,29 @@ export function WizardDocker({ onCreate, isDockerStandalone }: Props) {
onCreate={(environment) => onCreate(environment, 'localEndpoint')}
/>
);
case 'edgeAgent':
case 'edgeAgentStandard':
return (
<EdgeAgentTab
onCreate={(environment) => onCreate(environment, 'dockerEdgeAgent')}
onCreate={(environment) =>
onCreate(environment, 'dockerEdgeAgentStandard')
}
commands={{
linux: isDockerStandalone
? [commandsTabs.standaloneLinux]
: [commandsTabs.swarmLinux],
win: isDockerStandalone
? [commandsTabs.standaloneWindow]
: [commandsTabs.swarmWindows],
}}
/>
);
case 'edgeAgentAsync':
return (
<EdgeAgentTab
asyncMode
onCreate={(environment) =>
onCreate(environment, 'dockerEdgeAgentAsync')
}
commands={{
linux: isDockerStandalone
? [commandsTabs.standaloneLinux]

View file

@ -1,17 +1,17 @@
import { useState } from 'react';
import { Zap, Cloud, UploadCloud } from 'lucide-react';
import { Zap, UploadCloud } from 'lucide-react';
import _ from 'lodash';
import {
Environment,
EnvironmentCreationTypes,
} from '@/react/portainer/environments/types';
import { Environment } from '@/react/portainer/environments/types';
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import EdgeAgentStandardIcon from '@/react/edge/components/edge-agent-standard.svg?c';
import EdgeAgentAsyncIcon from '@/react/edge/components/edge-agent-async.svg?c';
import { BoxSelectorOption } from '@@/BoxSelector/types';
import { BoxSelector } from '@@/BoxSelector';
import { BEFeatureIndicator } from '@@/BEFeatureIndicator';
import { BadgeIcon } from '@@/BadgeIcon';
import { AnalyticsStateKey } from '../types';
import { EdgeAgentTab } from '../shared/EdgeAgentTab';
@ -24,37 +24,50 @@ interface Props {
onCreate(environment: Environment, analytics: AnalyticsStateKey): void;
}
const defaultOptions: BoxSelectorOption<EnvironmentCreationTypes>[] = [
type CreationType =
| 'agent'
| 'edgeAgentStandard'
| 'edgeAgentAsync'
| 'kubeconfig';
const defaultOptions: BoxSelectorOption<CreationType>[] = _.compact([
{
id: 'agent_endpoint',
icon: <BadgeIcon icon={Zap} size="3xl" />,
icon: Zap,
iconType: 'badge',
label: 'Agent',
value: EnvironmentCreationTypes.AgentEnvironment,
value: 'agent',
description: '',
},
{
id: 'edgeAgent',
icon: <BadgeIcon icon={Cloud} size="3xl" />,
label: 'Edge Agent',
id: 'edgeAgentStandard',
icon: EdgeAgentStandardIcon,
iconType: 'badge',
label: 'Edge Agent Standard',
description: '',
value: EnvironmentCreationTypes.EdgeAgentEnvironment,
hide: window.ddExtension,
value: 'edgeAgentStandard',
},
isBE && {
id: 'edgeAgentAsync',
icon: EdgeAgentAsyncIcon,
iconType: 'badge',
label: 'Edge Agent Async',
description: '',
value: 'edgeAgentAsync',
},
{
id: 'kubeconfig_endpoint',
icon: <BadgeIcon icon={UploadCloud} size="3xl" />,
icon: UploadCloud,
iconType: 'badge',
label: 'Import',
value: EnvironmentCreationTypes.KubeConfigEnvironment,
value: 'kubeconfig',
description: 'Import an existing Kubernetes config',
feature: FeatureId.K8S_CREATE_FROM_KUBECONFIG,
},
];
]);
export function WizardKubernetes({ onCreate }: Props) {
const options = useFilterEdgeOptionsIfNeeded(
defaultOptions,
EnvironmentCreationTypes.EdgeAgentEnvironment
);
const options = useFilterEdgeOptionsIfNeeded(defaultOptions, 'agent');
const [creationType, setCreationType] = useState(options[0].value);
@ -73,24 +86,34 @@ export function WizardKubernetes({ onCreate }: Props) {
</div>
);
function getTab(type: typeof options[number]['value']) {
function getTab(type: CreationType) {
switch (type) {
case EnvironmentCreationTypes.AgentEnvironment:
case 'agent':
return (
<AgentPanel
onCreate={(environment) => onCreate(environment, 'kubernetesAgent')}
/>
);
case EnvironmentCreationTypes.EdgeAgentEnvironment:
case 'edgeAgentStandard':
return (
<EdgeAgentTab
onCreate={(environment) =>
onCreate(environment, 'kubernetesEdgeAgent')
onCreate(environment, 'kubernetesEdgeAgentStandard')
}
commands={[{ ...commandsTabs.k8sLinux, label: 'Linux' }]}
/>
);
case EnvironmentCreationTypes.KubeConfigEnvironment:
case 'edgeAgentAsync':
return (
<EdgeAgentTab
asyncMode
onCreate={(environment) =>
onCreate(environment, 'kubernetesEdgeAgentAsync')
}
commands={[{ ...commandsTabs.k8sLinux, label: 'Linux' }]}
/>
);
case 'kubeconfig':
return (
<div className="border border-solid border-orange-1 px-1 py-5">
<BEFeatureIndicator

View file

@ -1,16 +1,30 @@
import { NameField } from '../../NameField';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { PortainerTunnelAddrField } from '@/react/portainer/common/PortainerTunnelAddrField';
import { PortainerUrlField } from '@/react/portainer/common/PortainerUrlField';
import { PortainerUrlField } from './PortainerUrlField';
import { NameField } from '../../NameField';
interface EdgeAgentFormProps {
readonly?: boolean;
asyncMode?: boolean;
}
export function EdgeAgentFieldset({ readonly }: EdgeAgentFormProps) {
export function EdgeAgentFieldset({ readonly, asyncMode }: EdgeAgentFormProps) {
return (
<>
<NameField readonly={readonly} />
<PortainerUrlField fieldName="portainerUrl" readonly={readonly} />
<PortainerUrlField
fieldName="portainerUrl"
readonly={readonly}
required
/>
{isBE && !asyncMode && (
<PortainerTunnelAddrField
fieldName="tunnelServerAddr"
readonly={readonly}
required
/>
)}
</>
);
}

View file

@ -3,9 +3,15 @@ import { Plug2 } from 'lucide-react';
import { Environment } from '@/react/portainer/environments/types';
import { useCreateEdgeAgentEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation';
import { baseHref } from '@/portainer/helpers/pathHelper';
import { Settings } from '@/react/portainer/settings/types';
import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField';
import { useCreateEdgeDeviceParam } from '@/react/portainer/environments/wizard/hooks/useCreateEdgeDeviceParam';
import {
EdgeAsyncIntervalsForm,
EDGE_ASYNC_INTERVAL_USE_DEFAULT,
} from '@/react/edge/components/EdgeAsyncIntervalsForm';
import { useSettings } from '@/react/portainer/settings/queries';
import { buildDefaultValue as buildTunnelDefaultValue } from '@/react/portainer/common/PortainerTunnelAddrField';
import { buildDefaultValue as buildApiUrlDefaultValue } from '@/react/portainer/common/PortainerUrlField';
import { FormSection } from '@@/form-components/FormSection';
import { LoadingButton } from '@@/buttons/LoadingButton';
@ -21,15 +27,27 @@ interface Props {
onCreate(environment: Environment): void;
readonly: boolean;
showGpus?: boolean;
asyncMode: boolean;
}
const initialValues = buildInitialValues();
export function EdgeAgentForm({ onCreate, readonly, showGpus = false }: Props) {
const createEdgeDevice = useCreateEdgeDeviceParam();
export function EdgeAgentForm({
onCreate,
readonly,
asyncMode,
showGpus = false,
}: Props) {
const settingsQuery = useSettings();
const createMutation = useCreateEdgeAgentEnvironmentMutation();
const validation = useValidationSchema();
const validation = useValidationSchema(asyncMode);
if (!settingsQuery.data) {
return null;
}
const settings = settingsQuery.data;
const initialValues = buildInitialValues(settings);
return (
<Formik<FormValues>
@ -40,15 +58,23 @@ export function EdgeAgentForm({ onCreate, readonly, showGpus = false }: Props) {
>
{({ isValid, setFieldValue, values }) => (
<Form>
<EdgeAgentFieldset readonly={readonly} />
<EdgeAgentFieldset readonly={readonly} asyncMode={asyncMode} />
<MoreSettingsSection>
<FormSection title="Check-in Intervals">
<EdgeCheckinIntervalField
readonly={readonly}
onChange={(value) => setFieldValue('pollFrequency', value)}
value={values.pollFrequency}
/>
{asyncMode ? (
<EdgeAsyncIntervalsForm
values={values.edge}
readonly={readonly}
onChange={(values) => setFieldValue('edge', values)}
/>
) : (
<EdgeCheckinIntervalField
readonly={readonly}
onChange={(value) => setFieldValue('pollFrequency', value)}
value={values.pollFrequency}
/>
)}
</FormSection>
{showGpus && <Hardware />}
</MoreSettingsSection>
@ -75,7 +101,13 @@ export function EdgeAgentForm({ onCreate, readonly, showGpus = false }: Props) {
function handleSubmit(values: typeof initialValues) {
createMutation.mutate(
{ ...values, isEdgeDevice: createEdgeDevice },
{
...values,
edge: {
...values.edge,
asyncMode,
},
},
{
onSuccess(environment) {
onCreate(environment);
@ -85,20 +117,22 @@ export function EdgeAgentForm({ onCreate, readonly, showGpus = false }: Props) {
}
}
export function buildInitialValues(): FormValues {
export function buildInitialValues(settings: Settings): FormValues {
return {
name: '',
portainerUrl: defaultPortainerUrl(),
portainerUrl: settings.EdgePortainerUrl || buildApiUrlDefaultValue(),
tunnelServerAddr:
settings.Edge.TunnelServerAddress || buildTunnelDefaultValue(),
pollFrequency: 0,
meta: {
groupId: 1,
tagIds: [],
},
edge: {
CommandInterval: EDGE_ASYNC_INTERVAL_USE_DEFAULT,
PingInterval: EDGE_ASYNC_INTERVAL_USE_DEFAULT,
SnapshotInterval: EDGE_ASYNC_INTERVAL_USE_DEFAULT,
},
gpus: [],
};
function defaultPortainerUrl() {
const baseHREF = baseHref();
return window.location.origin + (baseHREF !== '/' ? baseHREF : '');
}
}

View file

@ -1,21 +1,31 @@
import { number, object, SchemaOf } from 'yup';
import { number, object, SchemaOf, string } from 'yup';
import {
edgeAsyncIntervalsValidation,
EdgeAsyncIntervalsValues,
} from '@/react/edge/components/EdgeAsyncIntervalsForm';
import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
import { validation as urlValidation } from '@/react/portainer/common/PortainerTunnelAddrField';
import { validation as addressValidation } from '@/react/portainer/common/PortainerUrlField';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { metadataValidation } from '../../MetadataFieldset/validation';
import { useNameValidation } from '../../NameField';
import { validation as urlValidation } from './PortainerUrlField';
import { FormValues } from './types';
export function useValidationSchema(): SchemaOf<FormValues> {
export function useValidationSchema(asyncMode: boolean): SchemaOf<FormValues> {
const nameValidation = useNameValidation();
return object().shape({
name: nameValidation,
portainerUrl: urlValidation(),
tunnelServerAddr: asyncMode ? string() : addressValidation(),
pollFrequency: number().required(),
meta: metadataValidation(),
gpus: gpusListValidation(),
edge: isBE
? edgeAsyncIntervalsValidation()
: (null as unknown as SchemaOf<EdgeAsyncIntervalsValues>),
});
}

View file

@ -1,3 +1,4 @@
import { EdgeAsyncIntervalsValues } from '@/react/edge/components/EdgeAsyncIntervalsForm';
import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create';
@ -5,7 +6,10 @@ export interface FormValues {
name: string;
portainerUrl: string;
tunnelServerAddr?: string;
pollFrequency: number;
meta: EnvironmentMetadata;
gpus: Gpu[];
edge: EdgeAsyncIntervalsValues;
}

View file

@ -16,6 +16,7 @@ interface Props {
commands: CommandTab[] | Partial<Record<OS, CommandTab[]>>;
isNomadTokenVisible?: boolean;
showGpus?: boolean;
asyncMode?: boolean;
}
export function EdgeAgentTab({
@ -23,9 +24,9 @@ export function EdgeAgentTab({
commands,
isNomadTokenVisible,
showGpus = false,
asyncMode = false,
}: Props) {
const [edgeInfo, setEdgeInfo] = useState<EdgeInfo>();
const [formKey, clearForm] = useReducer((state) => state + 1, 0);
return (
@ -35,6 +36,7 @@ export function EdgeAgentTab({
readonly={!!edgeInfo}
key={formKey}
showGpus={showGpus}
asyncMode={asyncMode}
/>
{edgeInfo && (
@ -51,6 +53,7 @@ export function EdgeAgentTab({
edgeInfo={edgeInfo}
commands={commands}
isNomadTokenVisible={isNomadTokenVisible}
asyncMode={asyncMode}
/>
<hr />

View file

@ -1,13 +1,15 @@
export interface AnalyticsState {
dockerAgent: number;
dockerApi: number;
dockerEdgeAgent: number;
dockerEdgeAgentStandard: number;
dockerEdgeAgentAsync: number;
kubernetesAgent: number;
kubernetesEdgeAgent: number;
kubernetesEdgeAgentStandard: number;
kubernetesEdgeAgentAsync: number;
kaasAgent: number;
aciApi: number;
localEndpoint: number;
nomadEdgeAgent: number;
nomadEdgeAgentStandard: number;
}
export type AnalyticsStateKey = keyof AnalyticsState;

View file

@ -1,57 +1,29 @@
import { Field, Form, Formik } from 'formik';
import { Form, Formik } from 'formik';
import * as yup from 'yup';
import { useCallback, useEffect } from 'react';
import { useCallback } from 'react';
import { baseHref } from '@/portainer/helpers/pathHelper';
import { notifySuccess } from '@/portainer/services/notifications';
import { useUpdateSettingsMutation } from '@/react/portainer/settings/queries';
import { Settings } from '@/react/portainer/settings/types';
import { LoadingButton } from '@@/buttons/LoadingButton';
import { FormControl } from '@@/form-components/FormControl';
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
import { Input } from '@@/form-components/Input';
import { EnabledWaitingRoomSwitch } from './EnableWaitingRoomSwitch';
interface FormValues {
EdgePortainerUrl: string;
TrustOnFirstConnect: boolean;
EnableWaitingRoom: boolean;
}
const validation = yup.object({
TrustOnFirstConnect: yup.boolean(),
EdgePortainerUrl: yup
.string()
.test(
'url',
'URL should be a valid URI and cannot include localhost',
(value) => {
if (!value) {
return false;
}
try {
const url = new URL(value);
return !!url.hostname && url.hostname !== 'localhost';
} catch {
return false;
}
}
)
.required('URL is required'),
EnableWaitingRoom: yup.boolean(),
});
interface Props {
settings: Settings;
}
const defaultUrl = buildDefaultUrl();
export function AutoEnvCreationSettingsForm({ settings }: Props) {
const url = settings.EdgePortainerUrl;
const initialValues = {
EdgePortainerUrl: url || defaultUrl,
TrustOnFirstConnect: settings.TrustOnFirstConnect,
const initialValues: FormValues = {
EnableWaitingRoom: !settings.TrustOnFirstConnect,
};
const mutation = useUpdateSettingsMutation();
@ -60,24 +32,21 @@ export function AutoEnvCreationSettingsForm({ settings }: Props) {
const handleSubmit = useCallback(
(variables: Partial<FormValues>) => {
updateSettings(variables, {
onSuccess() {
notifySuccess(
'Success',
'Successfully updated Automatic Environment Creation settings'
);
},
});
updateSettings(
{ TrustOnFirstConnect: !variables.EnableWaitingRoom },
{
onSuccess() {
notifySuccess(
'Success',
'Successfully updated Automatic Environment Creation settings'
);
},
}
);
},
[updateSettings]
);
useEffect(() => {
if (!url && validation.isValidSync({ EdgePortainerUrl: defaultUrl })) {
updateSettings({ EdgePortainerUrl: defaultUrl });
}
}, [updateSettings, url]);
return (
<Formik<FormValues>
initialValues={initialValues}
@ -86,19 +55,8 @@ export function AutoEnvCreationSettingsForm({ settings }: Props) {
validateOnMount
enableReinitialize
>
{({ errors, isValid, dirty }) => (
{({ isValid, dirty }) => (
<Form className="form-horizontal">
<FormSectionTitle>Configuration</FormSectionTitle>
<FormControl
label="Portainer URL"
tooltip="URL of the Portainer instance that the agent will use to initiate the communications."
inputId="url-input"
errors={errors.EdgePortainerUrl}
>
<Field as={Input} id="url-input" name="EdgePortainerUrl" />
</FormControl>
<EnabledWaitingRoomSwitch />
<div className="form-group">
@ -107,6 +65,7 @@ export function AutoEnvCreationSettingsForm({ settings }: Props) {
loadingText="generating..."
isLoading={mutation.isLoading}
disabled={!isValid || !dirty}
className="!ml-0"
>
Save settings
</LoadingButton>
@ -117,8 +76,3 @@ export function AutoEnvCreationSettingsForm({ settings }: Props) {
</Formik>
);
}
function buildDefaultUrl() {
const baseHREF = baseHref();
return window.location.origin + (baseHREF !== '/' ? baseHREF : '');
}

View file

@ -1,69 +1,26 @@
import { useMutation } from 'react-query';
import { useEffect } from 'react';
import { Laptop } from 'lucide-react';
import { generateKey } from '@/react/portainer/environments/environment.service/edge';
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
import { useSettings } from '../../queries';
import { AutoEnvCreationSettingsForm } from './AutoEnvCreationSettingsForm';
const commands = {
linux: [
commandsTabs.k8sLinux,
commandsTabs.swarmLinux,
commandsTabs.standaloneLinux,
commandsTabs.nomadLinux,
],
win: [commandsTabs.swarmWindows, commandsTabs.standaloneWindow],
};
export function AutomaticEdgeEnvCreation() {
const edgeKeyMutation = useGenerateKeyMutation();
const { mutate: generateKey } = edgeKeyMutation;
const settingsQuery = useSettings();
const url = settingsQuery.data?.EdgePortainerUrl;
useEffect(() => {
if (url) {
generateKey();
}
}, [generateKey, url]);
if (!settingsQuery.data) {
return null;
}
const edgeKey = edgeKeyMutation.data;
const settings = settingsQuery.data;
return (
<Widget>
<WidgetTitle icon={Laptop} title="Automatic Edge Environment Creation" />
<WidgetBody>
<AutoEnvCreationSettingsForm settings={settingsQuery.data} />
{edgeKeyMutation.isLoading ? (
<div>Generating key for {url} ... </div>
) : (
edgeKey && (
<EdgeScriptForm
edgeInfo={{ key: edgeKey }}
commands={commands}
isNomadTokenVisible
/>
)
)}
<AutoEnvCreationSettingsForm settings={settings} />
</WidgetBody>
</Widget>
);
}
// using mutation because we want this action to run only when required
function useGenerateKeyMutation() {
return useMutation(generateKey);
}

View file

@ -7,17 +7,18 @@ import { buildConfirmButton } from '@@/modals/utils';
import { ModalType } from '@@/modals';
export function EnabledWaitingRoomSwitch() {
const [inputProps, meta, helpers] = useField<boolean>('TrustOnFirstConnect');
const [inputProps, meta, helpers] = useField<boolean>('EnableWaitingRoom');
return (
<FormControl
inputId="edge_waiting_room"
label="Disable Edge Environment Waiting Room"
label="Enable Edge Environment Waiting Room"
size="medium"
errors={meta.error}
>
<Switch
id="edge_waiting_room"
name="TrustOnFirstConnect"
name="EnableWaitingRoom"
className="space-right"
checked={inputProps.value}
onChange={handleChange}
@ -25,9 +26,9 @@ export function EnabledWaitingRoomSwitch() {
</FormControl>
);
async function handleChange(trust: boolean) {
if (!trust) {
helpers.setValue(false);
async function handleChange(enable: boolean) {
if (enable) {
helpers.setValue(true);
return;
}
@ -39,6 +40,10 @@ export function EnabledWaitingRoomSwitch() {
confirmButton: buildConfirmButton('Confirm', 'danger'),
});
helpers.setValue(!!confirmed);
if (!confirmed) {
return;
}
helpers.setValue(false);
}
}

View file

@ -5,9 +5,8 @@ import { Laptop } from 'lucide-react';
import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField';
import { EdgeAsyncIntervalsForm } from '@/react/edge/components/EdgeAsyncIntervalsForm';
import { notifySuccess } from '@/portainer/services/notifications';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { FormControl } from '@@/form-components/FormControl';
import { Switch } from '@@/form-components/SwitchField/Switch';
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
import { FormSection } from '@@/form-components/FormSection';
import { LoadingButton } from '@@/buttons/LoadingButton';
@ -45,7 +44,6 @@ export function DeploymentSyncOptions() {
const initialValues: FormValues = {
Edge: {
AsyncMode: settingsQuery.data.Edge.AsyncMode,
CommandInterval: settingsQuery.data.Edge.CommandInterval,
PingInterval: settingsQuery.data.Edge.PingInterval,
SnapshotInterval: settingsQuery.data.Edge.SnapshotInterval,
@ -63,8 +61,13 @@ export function DeploymentSyncOptions() {
onSubmit={handleSubmit}
key={formKey}
>
{({ errors, setFieldValue, values, isValid, dirty }) => (
{({ setFieldValue, values, isValid, dirty }) => (
<Form className="form-horizontal">
<TextTip color="blue">
Default values set here will be available to choose as an
option for edge environment creation
</TextTip>
<FormSection title="Check-in Intervals">
<EdgeCheckinIntervalField
value={values.EdgeAgentCheckinInterval}
@ -77,30 +80,7 @@ export function DeploymentSyncOptions() {
/>
</FormSection>
<FormControl
inputId="edge_async_mode"
label="Use Async mode by default"
size="small"
errors={errors?.Edge?.AsyncMode}
tooltip="Using Async allows the ability to define different ping,
snapshot and command frequencies."
>
<Switch
id="edge_async_mode"
name="edge_async_mode"
className="space-right"
checked={values.Edge.AsyncMode}
onChange={(e) =>
setFieldValue('Edge.AsyncMode', e.valueOf())
}
/>
</FormControl>
<TextTip color="orange">
Enabling Async disables the tunnel function.
</TextTip>
{values.Edge.AsyncMode && (
{isBE && (
<FormSection title="Async Check-in Intervals">
<EdgeAsyncIntervalsForm
values={values.Edge}
@ -111,20 +91,19 @@ export function DeploymentSyncOptions() {
</FormSection>
)}
<FormSection title="Actions">
<div className="form-group mt-5">
<div className="col-sm-12">
<LoadingButton
disabled={!isValid || !dirty}
data-cy="settings-deploySyncOptionsButton"
isLoading={settingsMutation.isLoading}
loadingText="Saving settings..."
>
Save settings
</LoadingButton>
</div>
<div className="form-group mt-5">
<div className="col-sm-12">
<LoadingButton
disabled={!isValid || !dirty}
className="!ml-0"
data-cy="settings-deploySyncOptionsButton"
isLoading={settingsMutation.isLoading}
loadingText="Saving settings..."
>
Save settings
</LoadingButton>
</div>
</FormSection>
</div>
</Form>
)}
</Formik>

View file

@ -3,7 +3,6 @@ export interface FormValues {
PingInterval: number;
SnapshotInterval: number;
CommandInterval: number;
AsyncMode: boolean;
};
EdgeAgentCheckinInterval: number;
}

View file

@ -1,57 +0,0 @@
import { useRouter } from '@uirouter/react';
import { Plus } from 'lucide-react';
import { usePublicSettings } from '@/react/portainer/settings/queries';
import { Button } from '@@/buttons';
import { openModal } from '@@/modals';
import { DeployTypePrompt } from './DeployTypePrompt';
enum DeployType {
FDO = 'FDO',
MANUAL = 'MANUAL',
}
export function AddDeviceButton() {
const router = useRouter();
const isFDOEnabledQuery = usePublicSettings({
select: (settings) => settings.IsFDOEnabled,
});
const isFDOEnabled = !!isFDOEnabledQuery.data;
return (
<Button onClick={handleNewDeviceClick} icon={Plus}>
Add Device
</Button>
);
async function handleNewDeviceClick() {
const result = await getDeployType();
switch (result) {
case DeployType.FDO:
router.stateService.go('portainer.endpoints.importDevice');
break;
case DeployType.MANUAL:
router.stateService.go('portainer.wizard.endpoints', {
edgeDevice: true,
});
break;
default:
break;
}
}
function getDeployType() {
if (!isFDOEnabled) {
return Promise.resolve(DeployType.MANUAL);
}
return askForDeployType();
}
}
function askForDeployType() {
return openModal(DeployTypePrompt, {});
}

View file

@ -1,69 +0,0 @@
import { useState } from 'react';
import { DeployType } from '@/react/nomad/jobs/JobsView/JobsDatatable/types';
import { OnSubmit } from '@@/modals';
import { Dialog } from '@@/modals/Dialog';
import { buildCancelButton, buildConfirmButton } from '@@/modals/utils';
export function DeployTypePrompt({
onSubmit,
}: {
onSubmit: OnSubmit<DeployType>;
}) {
const [deployType, setDeployType] = useState<DeployType>(DeployType.FDO);
return (
<Dialog
title="How would you like to add an Edge Device?"
message={
<>
<RadioInput
name="deployType"
value={DeployType.FDO}
label="Provision bare-metal using Intel FDO"
groupValue={deployType}
onChange={setDeployType}
/>
<RadioInput
name="deployType"
value={DeployType.MANUAL}
onChange={setDeployType}
groupValue={deployType}
label="Deploy agent manually"
/>
</>
}
buttons={[buildCancelButton(), buildConfirmButton()]}
onSubmit={(confirm) => onSubmit(confirm ? deployType : undefined)}
/>
);
}
function RadioInput<T extends number | string>({
value,
onChange,
label,
groupValue,
name,
}: {
value: T;
onChange: (value: T) => void;
label: string;
groupValue: T;
name: string;
}) {
return (
<label className="flex items-center gap-2">
<input
className="!m-0"
type="radio"
name={name}
value={value}
checked={groupValue === value}
onChange={() => onChange(value)}
/>
{label}
</label>
);
}

View file

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

View file

@ -1,19 +1,19 @@
import { Formik, Form } from 'formik';
import { Laptop } from 'lucide-react';
import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField';
import { Settings } from '@/react/portainer/settings/types';
import { PortainerUrlField } from '@/react/portainer/common/PortainerUrlField';
import { PortainerTunnelAddrField } from '@/react/portainer/common/PortainerTunnelAddrField';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { Switch } from '@@/form-components/SwitchField/Switch';
import { FormControl } from '@@/form-components/FormControl';
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
import { LoadingButton } from '@@/buttons/LoadingButton';
import { TextTip } from '@@/Tip/TextTip';
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
import { validationSchema } from './EdgeComputeSettings.validation';
import { FormValues } from './types';
import { AddDeviceButton } from './AddDeviceButton';
interface Props {
settings?: Settings;
@ -28,22 +28,16 @@ export function EdgeComputeSettings({ settings, onSubmit }: Props) {
const initialValues: FormValues = {
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
EdgePortainerUrl: settings.EdgePortainerUrl,
EdgeAgentCheckinInterval: settings.EdgeAgentCheckinInterval,
Edge: {
TunnelServerAddress: settings.Edge.TunnelServerAddress,
},
EnforceEdgeID: settings.EnforceEdgeID,
};
return (
<div className="row">
<Widget>
<WidgetTitle
icon={Laptop}
title={
<>
<span className="mr-3">Edge Compute settings</span>
{settings.EnableEdgeComputeFeatures && <AddDeviceButton />}
</>
}
/>
<WidgetTitle icon={Laptop} title="Edge Compute settings" />
<WidgetBody>
<Formik
@ -85,10 +79,21 @@ export function EdgeComputeSettings({ settings, onSubmit }: Props) {
</FormControl>
<TextTip color="blue">
When enabled, this will enable Portainer to execute Edge
Device features.
Enable this setting to use Portainer Edge Compute
capabilities.
</TextTip>
{isBE && values.EnableEdgeComputeFeatures && (
<>
<PortainerUrlField
fieldName="EdgePortainerUrl"
tooltip="URL of this Portainer instance that will be used by Edge agents to initiate the communications."
/>
<PortainerTunnelAddrField fieldName="Edge.TunnelServerAddress" />
</>
)}
<FormControl
inputId="edge_enforce_id"
label="Enforce use of Portainer generated Edge ID"
@ -107,18 +112,6 @@ export function EdgeComputeSettings({ settings, onSubmit }: Props) {
/>
</FormControl>
<FormSectionTitle>Check-in Intervals</FormSectionTitle>
<EdgeCheckinIntervalField
value={values.EdgeAgentCheckinInterval}
onChange={(value) =>
setFieldValue('EdgeAgentCheckinInterval', value)
}
isDefaultHidden
label="Edge agent default poll frequency"
tooltip="Interval used by default by each Edge agent to check in with the Portainer instance. Affects Edge environment management and Edge compute features."
/>
<div className="form-group mt-5">
<div className="col-sm-12">
<LoadingButton

View file

@ -2,5 +2,7 @@ export interface FormValues {
EnableEdgeComputeFeatures: boolean;
EdgePortainerUrl: string;
EnforceEdgeID: boolean;
EdgeAgentCheckinInterval: number;
Edge: {
TunnelServerAddress: string;
};
}

View file

@ -1,6 +1,9 @@
import { Settings } from '@/react/portainer/settings/types';
import { isBE } from '../../feature-flags/feature-flags.service';
import { EdgeComputeSettings } from './EdgeComputeSettings';
import { DeploymentSyncOptions } from './DeploymentSyncOptions/DeploymentSyncOptions';
import { AutomaticEdgeEnvCreation } from './AutomaticEdgeEnvCreation';
interface Props {
@ -13,7 +16,9 @@ export function EdgeComputeSettingsView({ settings, onSubmit }: Props) {
<div className="row">
<EdgeComputeSettings settings={settings} onSubmit={onSubmit} />
{process.env.PORTAINER_EDITION === 'BE' && <AutomaticEdgeEnvCreation />}
<DeploymentSyncOptions />
{isBE && <AutomaticEdgeEnvCreation />}
</div>
);
}