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:
parent
bc6a667a6b
commit
c819d4e7f7
59 changed files with 880 additions and 586 deletions
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -6,6 +6,6 @@ export interface Filter<T = number> {
|
|||
export enum ConnectionType {
|
||||
API,
|
||||
Agent,
|
||||
EdgeAgent,
|
||||
EdgeDevice,
|
||||
EdgeAgentStandard,
|
||||
EdgeAgentAsync,
|
||||
}
|
||||
|
|
65
app/react/portainer/common/PortainerTunnelAddrField.tsx
Normal file
65
app/react/portainer/common/PortainerTunnelAddrField.tsx
Normal 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`;
|
||||
}
|
|
@ -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}`;
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { AutomaticEdgeEnvCreation } from './AutomaticEdgeEnvCreation';
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { EdgeAutoCreateScriptViewWrapper as EdgeAutoCreateScriptView } from './EdgeAutoCreateScriptView';
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -22,7 +22,7 @@ export interface EnvironmentsQueryParams {
|
|||
tagsPartialMatch?: boolean;
|
||||
groupIds?: EnvironmentGroupId[];
|
||||
status?: EnvironmentStatus[];
|
||||
edgeDevice?: boolean;
|
||||
edgeAsync?: boolean;
|
||||
edgeDeviceUntrusted?: boolean;
|
||||
excludeSnapshots?: boolean;
|
||||
provisioned?: boolean;
|
||||
|
|
|
@ -15,6 +15,5 @@ export function useAgentDetails() {
|
|||
return {
|
||||
agentVersion,
|
||||
agentSecret: settingsQuery.data.AgentSecret,
|
||||
useEdgeAsyncMode: settingsQuery.data.Edge.AsyncMode,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 : '');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 : '');
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -3,7 +3,6 @@ export interface FormValues {
|
|||
PingInterval: number;
|
||||
SnapshotInterval: number;
|
||||
CommandInterval: number;
|
||||
AsyncMode: boolean;
|
||||
};
|
||||
EdgeAgentCheckinInterval: number;
|
||||
}
|
||||
|
|
|
@ -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, {});
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { AddDeviceButton } from './AddDeviceButton';
|
|
@ -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
|
||||
|
|
|
@ -2,5 +2,7 @@ export interface FormValues {
|
|||
EnableEdgeComputeFeatures: boolean;
|
||||
EdgePortainerUrl: string;
|
||||
EnforceEdgeID: boolean;
|
||||
EdgeAgentCheckinInterval: number;
|
||||
Edge: {
|
||||
TunnelServerAddress: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue