diff --git a/app/edge/components/EdgeCheckInIntervalField.tsx b/app/edge/components/EdgeCheckInIntervalField.tsx
index fda2c9ea1..0b4e3aca2 100644
--- a/app/edge/components/EdgeCheckInIntervalField.tsx
+++ b/app/edge/components/EdgeCheckInIntervalField.tsx
@@ -11,6 +11,7 @@ interface Props {
isDefaultHidden?: boolean;
label?: string;
tooltip?: string;
+ readonly?: boolean;
}
export const checkinIntervalOptions = [
@@ -34,6 +35,7 @@ export const checkinIntervalOptions = [
export function EdgeCheckinIntervalField({
value,
+ readonly,
onChange,
isDefaultHidden = false,
label = 'Poll frequency',
@@ -49,6 +51,7 @@ export function EdgeCheckinIntervalField({
onChange(parseInt(e.currentTarget.value, 10));
}}
options={options}
+ disabled={readonly}
/>
);
@@ -60,6 +63,7 @@ export const EdgeCheckinIntervalFieldAngular = r2a(EdgeCheckinIntervalField, [
'isDefaultHidden',
'tooltip',
'label',
+ 'readonly',
]);
function useOptions(isDefaultHidden: boolean) {
diff --git a/app/edge/components/EdgeScriptForm/EdgeScriptForm.tsx b/app/edge/components/EdgeScriptForm/EdgeScriptForm.tsx
deleted file mode 100644
index d4824bd9e..000000000
--- a/app/edge/components/EdgeScriptForm/EdgeScriptForm.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { useState } from 'react';
-
-import { useStatus } from '@/portainer/services/api/status.service';
-import { r2a } from '@/react-tools/react2angular';
-import { useSettings } from '@/portainer/settings/queries';
-
-import { EdgePropertiesForm } from './EdgePropertiesForm';
-import { ScriptTabs } from './ScriptTabs';
-import { EdgeProperties } from './types';
-
-interface Props {
- edgeKey: string;
- edgeId?: string;
-}
-
-export function EdgeScriptForm({ edgeKey, edgeId }: Props) {
- const [edgeProperties, setEdgeProperties] = useState({
- allowSelfSignedCertificates: true,
- envVars: '',
- edgeIdGenerator: '',
- os: 'linux',
- platform: 'k8s',
- });
-
- const settingsQuery = useSettings((settings) => settings.AgentSecret);
-
- const versionQuery = useStatus((status) => status.Version);
-
- if (!versionQuery.data) {
- return null;
- }
-
- const agentVersion = versionQuery.data;
- const agentSecret = settingsQuery.data;
-
- return (
- <>
-
- setEdgeProperties({ ...edgeProperties, [key]: value })
- }
- values={edgeProperties}
- hideIdGetter={edgeId !== undefined}
- />
-
-
- setEdgeProperties({ ...edgeProperties, platform })
- }
- edgeId={edgeId}
- agentSecret={agentSecret}
- />
- >
- );
-}
-
-export const EdgeScriptFormAngular = r2a(EdgeScriptForm, ['edgeKey', 'edgeId']);
diff --git a/app/edge/components/EdgeScriptForm/index.ts b/app/edge/components/EdgeScriptForm/index.ts
deleted file mode 100644
index a9c30ce0c..000000000
--- a/app/edge/components/EdgeScriptForm/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { EdgeScriptForm, EdgeScriptFormAngular } from './EdgeScriptForm';
diff --git a/app/edge/components/EdgeScriptForm/types.ts b/app/edge/components/EdgeScriptForm/types.ts
deleted file mode 100644
index ceba136d8..000000000
--- a/app/edge/components/EdgeScriptForm/types.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export type Platform = 'standalone' | 'swarm' | 'k8s';
-export type OS = 'win' | 'linux';
-
-export interface EdgeProperties {
- os: OS;
- allowSelfSignedCertificates: boolean;
- envVars: string;
- edgeIdGenerator: string;
- platform: Platform;
-}
diff --git a/app/edge/components/index.ts b/app/edge/components/index.ts
index 413afe19a..22d9cab86 100644
--- a/app/edge/components/index.ts
+++ b/app/edge/components/index.ts
@@ -1,9 +1,14 @@
import angular from 'angular';
+import { r2a } from '@/react-tools/react2angular';
+import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
+
import { EdgeCheckinIntervalFieldAngular } from './EdgeCheckInIntervalField';
-import { EdgeScriptFormAngular } from './EdgeScriptForm';
export const componentsModule = angular
.module('app.edge.components', [])
- .component('edgeCheckinIntervalField', EdgeCheckinIntervalFieldAngular)
- .component('edgeScriptForm', EdgeScriptFormAngular).name;
+ .component(
+ 'edgeScriptForm',
+ r2a(EdgeScriptForm, ['edgeInfo', 'commands', 'isNomadTokenVisible'])
+ )
+ .component('edgeCheckinIntervalField', EdgeCheckinIntervalFieldAngular).name;
diff --git a/app/portainer/components/BoxSelector/BoxSelector.css b/app/portainer/components/BoxSelector/BoxSelector.css
index a0950d82b..a5ce53c98 100644
--- a/app/portainer/components/BoxSelector/BoxSelector.css
+++ b/app/portainer/components/BoxSelector/BoxSelector.css
@@ -1,5 +1,4 @@
.boxselector_wrapper {
display: flex;
flex-flow: row wrap;
- margin: 5px;
}
diff --git a/app/portainer/components/BoxSelector/index.ts b/app/portainer/components/BoxSelector/index.ts
index 36858b8e2..2334bd23a 100644
--- a/app/portainer/components/BoxSelector/index.ts
+++ b/app/portainer/components/BoxSelector/index.ts
@@ -5,6 +5,8 @@ import { react2angular } from '@/react-tools/react2angular';
import { BoxSelector, buildOption } from './BoxSelector';
import { BoxSelectorAngular } from './BoxSelectorAngular';
+export { type BoxSelectorOption } from './types';
+
export { BoxSelector, buildOption };
const BoxSelectorReact = react2angular(BoxSelector, [
'value',
diff --git a/app/portainer/environments/types.ts b/app/portainer/environments/types.ts
index 5623eca58..98f123bed 100644
--- a/app/portainer/environments/types.ts
+++ b/app/portainer/environments/types.ts
@@ -60,6 +60,7 @@ export type Environment = {
TagIds: TagId[];
GroupId: EnvironmentGroupId;
EdgeID?: string;
+ EdgeKey: string;
EdgeCheckinInterval?: number;
QueryDate?: number;
LastCheckInDate?: number;
@@ -73,7 +74,6 @@ export type Environment = {
UserTrusted: boolean;
AMTDeviceGUID?: string;
};
-
/**
* TS reference of endpoint_create.go#EndpointCreationType iota
*/
diff --git a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.stories.tsx b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.stories.tsx
index c941bba0c..cf5888563 100644
--- a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.stories.tsx
+++ b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.stories.tsx
@@ -70,5 +70,6 @@ function mockEnvironment(type: EnvironmentType): Environment {
},
URL: 'url',
UserTrusted: false,
+ EdgeKey: '',
};
}
diff --git a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.test.tsx b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.test.tsx
index 6b4122ccc..81c51bd8d 100644
--- a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.test.tsx
+++ b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.test.tsx
@@ -23,6 +23,7 @@ test('loads component', async () => {
Kubernetes: { Snapshots: [] },
Id: 3,
UserTrusted: false,
+ EdgeKey: '',
};
const { getByText } = renderComponent(env);
@@ -44,6 +45,7 @@ test('shows group name', async () => {
Kubernetes: { Snapshots: [] },
Id: 3,
UserTrusted: false,
+ EdgeKey: '',
};
const { findByText } = renderComponent(env, { Name: groupName });
diff --git a/app/portainer/settings/edge-compute/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx b/app/portainer/settings/edge-compute/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx
index 84aa18fb0..0806419f6 100644
--- a/app/portainer/settings/edge-compute/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx
+++ b/app/portainer/settings/edge-compute/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx
@@ -2,12 +2,23 @@ import { useMutation } from 'react-query';
import { useEffect } from 'react';
import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget';
-import { EdgeScriptForm } from '@/edge/components/EdgeScriptForm';
import { generateKey } from '@/portainer/environments/environment.service/edge';
import { useSettings } from '@/portainer/settings/queries';
+import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
+import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
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;
@@ -39,7 +50,13 @@ export function AutomaticEdgeEnvCreation() {
{edgeKeyMutation.isLoading ? (
Generating key for {url} ...
) : (
- edgeKey &&
+ edgeKey && (
+
+ )
)}
diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html
index d2bed2129..a65bad2cd 100644
--- a/app/portainer/views/endpoints/edit/endpoint.html
+++ b/app/portainer/views/endpoints/edit/endpoint.html
@@ -54,7 +54,12 @@
-
+ Edge agent deployment script
+
Join token
diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js
index 39380fd8f..e25dafbbe 100644
--- a/app/portainer/views/endpoints/edit/endpointController.js
+++ b/app/portainer/views/endpoints/edit/endpointController.js
@@ -8,6 +8,8 @@ import { getAMTInfo } from 'Portainer/hostmanagement/open-amt/open-amt.service';
import { confirmAsync } from '@/portainer/services/modal.service/confirm';
import { isEdgeEnvironment } from '@/portainer/environments/utils';
+import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
+
angular.module('portainer.app').controller('EndpointController', EndpointController);
/* @ngInject */
@@ -29,6 +31,7 @@ function EndpointController(
$scope.onChangeCheckInInterval = onChangeCheckInInterval;
$scope.setFieldValue = setFieldValue;
$scope.onChangeTags = onChangeTags;
+ const isBE = process.env.PORTAINER_EDITION === 'BE';
$scope.state = {
uploadInProgress: false,
@@ -41,6 +44,11 @@ function EndpointController(
allowCreate: Authentication.isAdmin(),
allowSelfSignedCerts: true,
showAMTInfo: false,
+ showNomad: isBE,
+ edgeScriptCommands: {
+ linux: _.compact([commandsTabs.k8sLinux, commandsTabs.swarmLinux, commandsTabs.standaloneLinux, isBE && commandsTabs.nomadLinux]),
+ win: [commandsTabs.swarmWindows, commandsTabs.standaloneWindow],
+ },
};
$scope.formValues = {
diff --git a/app/react/edge/components/EdgeScriptForm/EdgeScriptForm.tsx b/app/react/edge/components/EdgeScriptForm/EdgeScriptForm.tsx
new file mode 100644
index 000000000..d5d91c9db
--- /dev/null
+++ b/app/react/edge/components/EdgeScriptForm/EdgeScriptForm.tsx
@@ -0,0 +1,70 @@
+import { Formik } from 'formik';
+
+import { OsSelector } from './OsSelector';
+import { CommandTab } from './scripts';
+import { ScriptTabs } from './ScriptTabs';
+import { EdgeScriptSettingsFieldset } from './EdgeScriptSettingsFieldset';
+import { validationSchema } from './EdgeScriptForm.validation';
+import { ScriptFormValues, OS, Platform, EdgeInfo } from './types';
+
+const edgePropertiesFormInitialValues: ScriptFormValues = {
+ allowSelfSignedCertificates: true,
+ envVars: '',
+ os: 'linux' as OS,
+ platform: 'k8s' as Platform,
+ nomadToken: '',
+ authEnabled: true,
+};
+
+interface Props {
+ edgeInfo: EdgeInfo;
+ commands: CommandTab[] | Partial>;
+ isNomadTokenVisible?: boolean;
+}
+
+export function EdgeScriptForm({
+ edgeInfo,
+ commands,
+ isNomadTokenVisible,
+}: Props) {
+ const showOsSelector = !(commands instanceof Array);
+
+ return (
+
+
validationSchema(isNomadTokenVisible)}
+ onSubmit={() => {}}
+ >
+ {({ values, setFieldValue }) => (
+ <>
+
+
+ {showOsSelector && (
+ setFieldValue('os', value)}
+ />
+ )}
+
+ setFieldValue('platform', platform)
+ }
+ />
+
+ >
+ )}
+
+
+ );
+}
diff --git a/app/react/edge/components/EdgeScriptForm/EdgeScriptForm.validation.tsx b/app/react/edge/components/EdgeScriptForm/EdgeScriptForm.validation.tsx
new file mode 100644
index 000000000..69be18263
--- /dev/null
+++ b/app/react/edge/components/EdgeScriptForm/EdgeScriptForm.validation.tsx
@@ -0,0 +1,11 @@
+import { object, boolean, string } from 'yup';
+
+import { validation as nomadTokenValidation } from './NomadTokenField';
+
+export function validationSchema(isNomadTokenVisible?: boolean) {
+ return object().shape({
+ allowSelfSignedCertificates: boolean(),
+ envVars: string(),
+ ...(isNomadTokenVisible ? nomadTokenValidation() : {}),
+ });
+}
diff --git a/app/edge/components/EdgeScriptForm/EdgePropertiesForm.tsx b/app/react/edge/components/EdgeScriptForm/EdgeScriptSettingsFieldset.tsx
similarity index 58%
rename from app/edge/components/EdgeScriptForm/EdgePropertiesForm.tsx
rename to app/react/edge/components/EdgeScriptForm/EdgeScriptSettingsFieldset.tsx
index 29fa16105..31ead6309 100644
--- a/app/edge/components/EdgeScriptForm/EdgePropertiesForm.tsx
+++ b/app/react/edge/components/EdgeScriptForm/EdgeScriptSettingsFieldset.tsx
@@ -1,32 +1,26 @@
+import { useFormikContext, Field } from 'formik';
+
import { FormControl } from '@/portainer/components/form-components/FormControl';
import { Input } from '@/portainer/components/form-components/Input';
-import { FormSectionTitle } from '@/portainer/components/form-components/FormSectionTitle';
import { SwitchField } from '@/portainer/components/form-components/SwitchField';
import { TextTip } from '@/portainer/components/Tip/TextTip';
-import { OsSelector } from './OsSelector';
-import { EdgeProperties } from './types';
+import { NomadTokenField } from './NomadTokenField';
+import { ScriptFormValues } from './types';
interface Props {
- setFieldValue(key: string, value: T): void;
- values: EdgeProperties;
- hideIdGetter: boolean;
+ isNomadTokenVisible?: boolean;
+ hideIdGetter?: boolean;
}
-export function EdgePropertiesForm({
- setFieldValue,
- values,
+export function EdgeScriptSettingsFieldset({
+ isNomadTokenVisible,
hideIdGetter,
}: Props) {
+ const { values, setFieldValue } = useFormikContext();
+
return (
-
+ >
);
}
diff --git a/app/react/edge/components/EdgeScriptForm/NomadTokenField.tsx b/app/react/edge/components/EdgeScriptForm/NomadTokenField.tsx
new file mode 100644
index 000000000..cdf61d0f4
--- /dev/null
+++ b/app/react/edge/components/EdgeScriptForm/NomadTokenField.tsx
@@ -0,0 +1,53 @@
+import { Field, useFormikContext } from 'formik';
+import { string, boolean } from 'yup';
+
+import { FormControl } from '@/portainer/components/form-components/FormControl';
+import { SwitchField } from '@/portainer/components/form-components/SwitchField';
+import { Input } from '@/portainer/components/form-components/Input';
+
+import { ScriptFormValues } from './types';
+
+export function NomadTokenField() {
+ const { values, setFieldValue, errors } =
+ useFormikContext();
+
+ return (
+ <>
+
+
+ {
+ if (!value) {
+ setFieldValue('nomadToken', '');
+ }
+ setFieldValue('authEnabled', value);
+ }}
+ label="Nomad Authentication Enabled"
+ tooltip="Nomad authentication is only required if you have ACL enabled"
+ />
+
+
+
+ {values.authEnabled && (
+
+
+
+ )}
+ >
+ );
+}
+
+export function validation() {
+ return {
+ nomadToken: string().when('authEnabled', {
+ is: true,
+ then: string().required('Token is required'),
+ }),
+ authEnabled: boolean(),
+ };
+}
diff --git a/app/edge/components/EdgeScriptForm/OsSelector.tsx b/app/react/edge/components/EdgeScriptForm/OsSelector.tsx
similarity index 100%
rename from app/edge/components/EdgeScriptForm/OsSelector.tsx
rename to app/react/edge/components/EdgeScriptForm/OsSelector.tsx
diff --git a/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx b/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx
new file mode 100644
index 000000000..ec19f3d90
--- /dev/null
+++ b/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx
@@ -0,0 +1,68 @@
+import { useEffect } from 'react';
+
+import { Code } from '@/portainer/components/Code';
+import { CopyButton } from '@/portainer/components/Button/CopyButton';
+import { NavTabs } from '@/portainer/components/NavTabs/NavTabs';
+import { useAgentDetails } from '@/portainer/environments/queries/useAgentDetails';
+
+import { ScriptFormValues, Platform } from './types';
+import { CommandTab } from './scripts';
+
+interface Props {
+ values: ScriptFormValues;
+ edgeKey: string;
+ edgeId?: string;
+ commands: CommandTab[];
+ platform?: Platform;
+ onPlatformChange?(platform: Platform): void;
+}
+
+export function ScriptTabs({
+ values,
+ edgeKey,
+ edgeId,
+ commands,
+ platform,
+ onPlatformChange = () => {},
+}: Props) {
+ const agentDetails = useAgentDetails();
+
+ useEffect(() => {
+ if (commands.length > 0 && commands.every((p) => p.id !== platform)) {
+ onPlatformChange(commands[0].id);
+ }
+ }, [platform, onPlatformChange, commands]);
+
+ if (!agentDetails) {
+ return null;
+ }
+
+ const { agentSecret, agentVersion } = agentDetails;
+
+ const options = commands.map((c) => {
+ const cmd = c.command(agentVersion, edgeKey, values, edgeId, agentSecret);
+
+ return {
+ id: c.id,
+ label: c.label,
+ children: (
+ <>
+ {cmd}
+ Copy
+ >
+ ),
+ };
+ });
+
+ return (
+
+
+ onPlatformChange(id)}
+ />
+
+
+ );
+}
diff --git a/app/react/edge/components/EdgeScriptForm/index.ts b/app/react/edge/components/EdgeScriptForm/index.ts
new file mode 100644
index 000000000..f76d93c69
--- /dev/null
+++ b/app/react/edge/components/EdgeScriptForm/index.ts
@@ -0,0 +1 @@
+export { EdgeScriptForm } from './EdgeScriptForm';
diff --git a/app/edge/components/EdgeScriptForm/ScriptTabs.tsx b/app/react/edge/components/EdgeScriptForm/scripts.ts
similarity index 51%
rename from app/edge/components/EdgeScriptForm/ScriptTabs.tsx
rename to app/react/edge/components/EdgeScriptForm/scripts.ts
index dc300a941..7a74693ef 100644
--- a/app/edge/components/EdgeScriptForm/ScriptTabs.tsx
+++ b/app/react/edge/components/EdgeScriptForm/scripts.ts
@@ -1,111 +1,50 @@
-import { useEffect } from 'react';
import _ from 'lodash';
-import { Code } from '@/portainer/components/Code';
-import { CopyButton } from '@/portainer/components/Button/CopyButton';
-import { NavTabs } from '@/portainer/components/NavTabs/NavTabs';
import { getAgentShortVersion } from '@/portainer/views/endpoints/helpers';
-import { EdgeProperties, Platform } from './types';
+import { ScriptFormValues, Platform } from './types';
-const commandsByOs = {
- linux: [
- {
- id: 'k8s',
- label: 'Kubernetes',
- command: buildKubernetesCommand,
- },
- {
- id: 'swarm',
- label: 'Docker Swarm',
- command: buildLinuxSwarmCommand,
- },
- {
- id: 'standalone',
- label: 'Docker Standalone',
- command: buildLinuxStandaloneCommand,
- },
- ],
- win: [
- {
- id: 'swarm',
- label: 'Docker Swarm',
- command: buildWindowsSwarmCommand,
- },
- {
- id: 'standalone',
- label: 'Docker Standalone',
- command: buildWindowsStandaloneCommand,
- },
- ],
+type CommandGenerator = (
+ agentVersion: string,
+ edgeKey: string,
+ properties: ScriptFormValues,
+ edgeId?: string,
+ agentSecret?: string
+) => string;
+
+export type CommandTab = {
+ id: Platform;
+ label: string;
+ command: CommandGenerator;
};
-interface Props {
- values: EdgeProperties;
- edgeKey: string;
- agentVersion: string;
- edgeId?: string;
- agentSecret?: string;
- onPlatformChange(platform: Platform): void;
-}
-
-export function ScriptTabs({
- agentVersion,
- values,
- edgeKey,
- edgeId,
- agentSecret,
- onPlatformChange,
-}: Props) {
- const {
- os,
- allowSelfSignedCertificates,
- edgeIdGenerator,
- envVars,
- platform,
- } = values;
-
- useEffect(() => {
- if (!commandsByOs[os].find((p) => p.id === platform)) {
- onPlatformChange('swarm');
- }
- }, [os, platform, onPlatformChange]);
-
- const options = commandsByOs[os].map((c) => {
- const cmd = c.command(
- agentVersion,
- edgeIdGenerator,
- edgeKey,
- allowSelfSignedCertificates,
- envVars,
- edgeId,
- agentSecret
- );
-
- return {
- id: c.id,
- label: c.label,
- children: (
- <>
- {cmd}
- Copy
- >
- ),
- };
- });
-
- return (
-
-
- onPlatformChange(id)}
- />
-
-
- );
-}
+export const commandsTabs: Record = {
+ k8sLinux: {
+ id: 'k8s',
+ label: 'Kubernetes',
+ command: buildLinuxKubernetesCommand,
+ },
+ swarmLinux: {
+ id: 'swarm',
+ label: 'Docker Swarm',
+ command: buildLinuxSwarmCommand,
+ },
+ standaloneLinux: {
+ id: 'standalone',
+ label: 'Docker Standalone',
+ command: buildLinuxStandaloneCommand,
+ },
+ swarmWindows: {
+ id: 'swarm',
+ label: 'Docker Swarm',
+ command: buildWindowsSwarmCommand,
+ },
+ standaloneWindow: {
+ id: 'standalone',
+ label: 'Docker Standalone',
+ command: buildWindowsStandaloneCommand,
+ },
+} as const;
function buildDockerEnvVars(envVars: string, defaultVars: string[]) {
const vars = defaultVars.concat(
@@ -115,26 +54,28 @@ function buildDockerEnvVars(envVars: string, defaultVars: string[]) {
return vars.map((s) => `-e ${s}`).join(' \\\n ');
}
-function buildLinuxStandaloneCommand(
+export function buildLinuxStandaloneCommand(
agentVersion: string,
- edgeIdScript: string,
edgeKey: string,
- allowSelfSignedCerts: boolean,
- envVars: string,
+ properties: ScriptFormValues,
edgeId?: string,
agentSecret?: string
) {
+ const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
+
const env = buildDockerEnvVars(
envVars,
buildDefaultEnvVars(
edgeKey,
- allowSelfSignedCerts,
- !edgeIdScript ? edgeId : undefined,
+ allowSelfSignedCertificates,
+ !edgeIdGenerator ? edgeId : undefined,
agentSecret
)
);
- return `${edgeIdScript ? `PORTAINER_EDGE_ID=$(${edgeIdScript}) \n\n` : ''}\
+ return `${
+ edgeIdGenerator ? `PORTAINER_EDGE_ID=$(${edgeIdGenerator}) \n\n` : ''
+ }\
docker run -d \\
-v /var/run/docker.sock:/var/run/docker.sock \\
-v /var/lib/docker/volumes:/var/lib/docker/volumes \\
@@ -147,27 +88,29 @@ docker run -d \\
`;
}
-function buildWindowsStandaloneCommand(
+export function buildWindowsStandaloneCommand(
agentVersion: string,
- edgeIdScript: string,
edgeKey: string,
- allowSelfSignedCerts: boolean,
- envVars: string,
+ properties: ScriptFormValues,
edgeId?: string,
agentSecret?: string
) {
+ const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
+
const env = buildDockerEnvVars(
envVars,
buildDefaultEnvVars(
edgeKey,
- allowSelfSignedCerts,
- edgeIdScript ? '$Env:PORTAINER_EDGE_ID' : edgeId,
+ allowSelfSignedCertificates,
+ edgeIdGenerator ? '$Env:PORTAINER_EDGE_ID' : edgeId,
agentSecret
)
);
return `${
- edgeIdScript ? `$Env:PORTAINER_EDGE_ID = "@(${edgeIdScript})" \n\n` : ''
+ edgeIdGenerator
+ ? `$Env:PORTAINER_EDGE_ID = "@(${edgeIdGenerator})" \n\n`
+ : ''
}\
docker run -d \\
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\
@@ -180,30 +123,32 @@ docker run -d \\
`;
}
-function buildLinuxSwarmCommand(
+export function buildLinuxSwarmCommand(
agentVersion: string,
- edgeIdScript: string,
edgeKey: string,
- allowSelfSignedCerts: boolean,
- envVars: string,
+ properties: ScriptFormValues,
edgeId?: string,
agentSecret?: string
) {
+ const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
+
const env = buildDockerEnvVars(envVars, [
...buildDefaultEnvVars(
edgeKey,
- allowSelfSignedCerts,
- !edgeIdScript ? edgeId : undefined,
+ allowSelfSignedCertificates,
+ !edgeIdGenerator ? edgeId : undefined,
agentSecret
),
'AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent',
]);
- return `${edgeIdScript ? `PORTAINER_EDGE_ID=$(${edgeIdScript}) \n\n` : ''}\
+ return `${
+ edgeIdGenerator ? `PORTAINER_EDGE_ID=$(${edgeIdGenerator}) \n\n` : ''
+ }\
docker network create \\
--driver overlay \\
portainer_agent_network;
-
+
docker service create \\
--name portainer_edge_agent \\
--network portainer_agent_network \\
@@ -218,28 +163,30 @@ docker service create \\
`;
}
-function buildWindowsSwarmCommand(
+export function buildWindowsSwarmCommand(
agentVersion: string,
- edgeIdScript: string,
edgeKey: string,
- allowSelfSignedCerts: boolean,
- envVars: string,
+ properties: ScriptFormValues,
edgeId?: string,
agentSecret?: string
) {
+ const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
+
const env = buildDockerEnvVars(envVars, [
...buildDefaultEnvVars(
edgeKey,
- allowSelfSignedCerts,
- edgeIdScript ? '$Env:PORTAINER_EDGE_ID' : edgeId,
+ allowSelfSignedCertificates,
+ edgeIdGenerator ? '$Env:PORTAINER_EDGE_ID' : edgeId,
agentSecret
),
'AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent',
]);
return `${
- edgeIdScript ? `$Env:PORTAINER_EDGE_ID = "@(${edgeIdScript})" \n\n` : ''
- }
+ edgeIdGenerator
+ ? `$Env:PORTAINER_EDGE_ID = "@(${edgeIdGenerator})" \n\n`
+ : ''
+ }\
docker network create \\
--driver overlay \\
portainer_agent_network;
@@ -257,24 +204,24 @@ docker service create \\
`;
}
-function buildKubernetesCommand(
+export function buildLinuxKubernetesCommand(
agentVersion: string,
- edgeIdScript: string,
edgeKey: string,
- allowSelfSignedCerts: boolean,
- envVars: string,
+ properties: ScriptFormValues,
edgeId?: string,
- agentSecret = ''
+ agentSecret?: string
) {
- const agentShortVersion = getAgentShortVersion(agentVersion);
- const idEnvVar = edgeIdScript
- ? `PORTAINER_EDGE_ID=$(${edgeIdScript}) \n\n`
- : '';
- const envVarsTrimmed = envVars.trim();
- const edgeIdVar = !edgeIdScript && edgeId ? edgeId : '$PORTAINER_EDGE_ID';
- const selfSigned = allowSelfSignedCerts ? '1' : '0';
+ const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
- return `${idEnvVar}curl https://downloads.portainer.io/ce${agentShortVersion}/portainer-edge-agent-setup.sh | bash -s -- "${edgeIdVar}" "${edgeKey}" "${selfSigned}" "${agentSecret}" "${envVarsTrimmed}"`;
+ const agentShortVersion = getAgentShortVersion(agentVersion);
+ const envVarsTrimmed = envVars.trim();
+ const idEnvVar = edgeIdGenerator
+ ? `PORTAINER_EDGE_ID=$(${edgeIdGenerator}) \n\n`
+ : '';
+ const edgeIdVar = !edgeIdGenerator && edgeId ? edgeId : '$PORTAINER_EDGE_ID';
+ const selfSigned = allowSelfSignedCertificates ? '1' : '0';
+
+ return `${idEnvVar}curl https://downloads.portainer.io/ee${agentShortVersion}/portainer-edge-agent-setup.sh | bash -s -- "${edgeIdVar}" "${edgeKey}" "${selfSigned}" "${agentSecret}" "${envVarsTrimmed}"`;
}
function buildDefaultEnvVars(
diff --git a/app/react/edge/components/EdgeScriptForm/types.ts b/app/react/edge/components/EdgeScriptForm/types.ts
new file mode 100644
index 000000000..1f2e6801e
--- /dev/null
+++ b/app/react/edge/components/EdgeScriptForm/types.ts
@@ -0,0 +1,20 @@
+export type Platform = 'standalone' | 'swarm' | 'k8s' | 'nomad';
+export type OS = 'win' | 'linux';
+
+export interface ScriptFormValues {
+ nomadToken: string;
+ authEnabled: boolean;
+
+ allowSelfSignedCertificates: boolean;
+ envVars: string;
+
+ os: OS;
+ platform: Platform;
+
+ edgeIdGenerator?: string;
+}
+
+export interface EdgeInfo {
+ id?: string;
+ key: string;
+}
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx
index 9df3d9958..431465d14 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx
@@ -193,6 +193,7 @@ function useAnalyticsState() {
aciApi: 0,
localEndpoint: 0,
nomadEdgeAgent: 0,
+ dockerEdgeAgent: 0,
});
return { analytics, setAnalytics };
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardAzure/WizardAzure.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardAzure/WizardAzure.tsx
index b5f50e7d5..04b89059a 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardAzure/WizardAzure.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardAzure/WizardAzure.tsx
@@ -13,8 +13,8 @@ import { EnvironmentMetadata } from '@/portainer/environments/environment.servic
import { NameField, nameValidation } from '../shared/NameField';
import { AnalyticsStateKey } from '../types';
-import { MetadataFieldset } from '../shared/MetadataFieldset';
import { metadataValidation } from '../shared/MetadataFieldset/validation';
+import { MoreSettingsSection } from '../shared/MoreSettingsSection';
interface FormValues {
name: string;
@@ -109,7 +109,7 @@ export function WizardAzure({ onCreate }: Props) {
/>
-
+
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx
index 68525e5fa..8c025b82e 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx
@@ -12,7 +12,7 @@ import {
} from '@/portainer/environments/types';
import { NameField } from '../../shared/NameField';
-import { MetadataFieldset } from '../../shared/MetadataFieldset';
+import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
import { validation } from './APIForm.validation';
import { FormValues } from './types';
@@ -66,7 +66,7 @@ export function APIForm({ onCreate }: Props) {
-
+
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APITab.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APITab.tsx
index de2378edf..90834532b 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APITab.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APITab.tsx
@@ -12,7 +12,7 @@ export function APITab({ onCreate }: Props) {
<>
-
+
>
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/DeploymentScripts.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/DeploymentScripts.tsx
index 6c59dac19..8d95eab4d 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/DeploymentScripts.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/DeploymentScripts.tsx
@@ -55,9 +55,7 @@ function DeployCode({ code }: DeployCodeProps) {
{code}
-
- Copy command
-
+
Copy command
>
);
}
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/AgentTab/AgentTab.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/AgentTab/AgentTab.tsx
index 5a5c86bc0..ec5a3b8e8 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/AgentTab/AgentTab.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/AgentTab/AgentTab.tsx
@@ -13,7 +13,7 @@ export function AgentTab({ onCreate }: Props) {
<>
-
+
>
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx
index 4998480d9..cb7013b46 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx
@@ -10,7 +10,7 @@ import { SwitchField } from '@/portainer/components/form-components/SwitchField'
import { Environment } from '@/portainer/environments/types';
import { NameField } from '../../shared/NameField';
-import { MetadataFieldset } from '../../shared/MetadataFieldset';
+import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
import { validation } from './SocketForm.validation';
import { FormValues } from './types';
@@ -44,8 +44,7 @@ export function SocketForm({ onCreate }: Props) {
-
-
+
-
+
>
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx
index 0a2f85a40..862ca604a 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx
@@ -1,9 +1,14 @@
import { useState } from 'react';
-import { BoxSelector, buildOption } from '@/portainer/components/BoxSelector';
+import {
+ BoxSelector,
+ BoxSelectorOption,
+} from '@/portainer/components/BoxSelector';
import { Environment } from '@/portainer/environments/types';
+import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
import { AnalyticsStateKey } from '../types';
+import { EdgeAgentTab } from '../shared/EdgeAgentTab';
import { AgentTab } from './AgentTab';
import { APITab } from './APITab';
@@ -13,16 +18,41 @@ interface Props {
onCreate(environment: Environment, analytics: AnalyticsStateKey): void;
}
-const options = [
- buildOption('Agent', 'fa fa-bolt', 'Agent', '', 'agent'),
- buildOption('API', 'fa fa-cloud', 'API', '', 'api'),
- buildOption('Socket', 'fab fa-docker', 'Socket', '', 'socket'),
+const options: BoxSelectorOption<'agent' | 'api' | 'socket' | 'edgeAgent'>[] = [
+ {
+ id: 'agent',
+ icon: 'fa fa-bolt',
+ label: 'Agent',
+ description: '',
+ value: 'agent',
+ },
+ {
+ id: 'api',
+ icon: 'fa fa-cloud',
+ label: 'API',
+ description: '',
+ value: 'api',
+ },
+ {
+ id: 'socket',
+ icon: 'fab fa-docker',
+ label: 'Socket',
+ description: '',
+ value: 'socket',
+ },
+ {
+ id: 'edgeAgent',
+ icon: 'fa fa-cloud', // Todo cloud with docker
+ label: 'Edge Agent',
+ description: '',
+ value: 'edgeAgent',
+ },
];
export function WizardDocker({ onCreate }: Props) {
const [creationType, setCreationType] = useState(options[0].value);
- const form = getForm(creationType);
+ const tab = getTab(creationType);
return (
@@ -37,11 +67,11 @@ export function WizardDocker({ onCreate }: Props) {
- {form}
+ {tab}
);
- function getForm(creationType: 'agent' | 'api' | 'socket') {
+ function getTab(creationType: 'agent' | 'api' | 'socket' | 'edgeAgent') {
switch (creationType) {
case 'agent':
return (
@@ -61,6 +91,16 @@ export function WizardDocker({ onCreate }: Props) {
onCreate={(environment) => onCreate(environment, 'localEndpoint')}
/>
);
+ case 'edgeAgent':
+ return (
+
onCreate(environment, 'dockerEdgeAgent')}
+ commands={{
+ linux: [commandsTabs.swarmLinux, commandsTabs.standaloneLinux],
+ win: [commandsTabs.swarmWindows, commandsTabs.standaloneWindow],
+ }}
+ />
+ );
default:
return null;
}
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/AgentPanel.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/AgentPanel.tsx
index 53aedf407..dec3b9f56 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/AgentPanel.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/AgentPanel.tsx
@@ -1,12 +1,11 @@
import { Environment } from '@/portainer/environments/types';
-import { AgentForm } from '../shared/AgentForm/AgentForm';
-import { AnalyticsStateKey } from '../types';
+import { AgentForm } from '../shared/AgentForm';
import { DeploymentScripts } from './DeploymentScripts';
interface Props {
- onCreate(environment: Environment, analytics: AnalyticsStateKey): void;
+ onCreate(environment: Environment): void;
}
export function AgentPanel({ onCreate }: Props) {
@@ -14,9 +13,9 @@ export function AgentPanel({ onCreate }: Props) {
<>
- onCreate(environment, 'kubernetesAgent')}
- />
+
>
);
}
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/DeploymentScripts.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/DeploymentScripts.tsx
index c09634651..d2b1c745b 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/DeploymentScripts.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/DeploymentScripts.tsx
@@ -106,9 +106,7 @@ function DeployCode({
)}
{code}
-
- Copy command
-
+ Copy command
>
);
}
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/WizardKubernetes.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/WizardKubernetes.tsx
index 9c9363d9a..e7956ec08 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/WizardKubernetes.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/WizardKubernetes.tsx
@@ -6,8 +6,10 @@ import {
EnvironmentCreationTypes,
} from '@/portainer/environments/types';
import { BoxSelectorOption } from '@/portainer/components/BoxSelector/types';
+import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
import { AnalyticsStateKey } from '../types';
+import { EdgeAgentTab } from '../shared/EdgeAgentTab';
import { AgentPanel } from './AgentPanel';
@@ -15,21 +17,30 @@ interface Props {
onCreate(environment: Environment, analytics: AnalyticsStateKey): void;
}
-const options: BoxSelectorOption[] =
- [
- {
- id: 'agent_endpoint',
- icon: 'fa fa-bolt',
- label: 'Agent',
- value: EnvironmentCreationTypes.AgentEnvironment,
- description: '',
- },
- ];
+const options: BoxSelectorOption<
+ | EnvironmentCreationTypes.AgentEnvironment
+ | EnvironmentCreationTypes.EdgeAgentEnvironment
+>[] = [
+ {
+ id: 'agent_endpoint',
+ icon: 'fa fa-bolt',
+ label: 'Agent',
+ value: EnvironmentCreationTypes.AgentEnvironment,
+ description: '',
+ },
+ {
+ id: 'edgeAgent',
+ icon: 'fa fa-cloud', // Todo cloud with docker
+ label: 'Edge Agent',
+ description: '',
+ value: EnvironmentCreationTypes.EdgeAgentEnvironment,
+ },
+];
export function WizardKubernetes({ onCreate }: Props) {
const [creationType, setCreationType] = useState(options[0].value);
- const Component = getPanel(creationType);
+ const tab = getTab(creationType);
return (
@@ -40,16 +51,29 @@ export function WizardKubernetes({ onCreate }: Props) {
radioName="creation-type"
/>
-
+ {tab}
);
-}
-function getPanel(type: typeof options[number]['value']) {
- switch (type) {
- case EnvironmentCreationTypes.AgentEnvironment:
- return AgentPanel;
- default:
- throw new Error('Creation type not supported');
+ function getTab(type: typeof options[number]['value']) {
+ switch (type) {
+ case EnvironmentCreationTypes.AgentEnvironment:
+ return (
+ onCreate(environment, 'kubernetesAgent')}
+ />
+ );
+ case EnvironmentCreationTypes.EdgeAgentEnvironment:
+ return (
+
+ onCreate(environment, 'kubernetesEdgeAgent')
+ }
+ commands={[{ ...commandsTabs.k8sLinux, label: 'Linux' }]}
+ />
+ );
+ default:
+ throw new Error('Creation type not supported');
+ }
}
}
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx
index 5012f84db..252a679f5 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx
@@ -8,7 +8,7 @@ import { Environment } from '@/portainer/environments/types';
import { CreateAgentEnvironmentValues } from '@/portainer/environments/environment.service/create';
import { NameField } from '../NameField';
-import { MetadataFieldset } from '../MetadataFieldset';
+import { MoreSettingsSection } from '../MoreSettingsSection';
import { EnvironmentUrlField } from './EnvironmentUrlField';
import { validation } from './AgentForm.validation';
@@ -44,7 +44,7 @@ export function AgentForm({ onCreate }: Props) {
-
+
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentFieldset.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentFieldset.tsx
new file mode 100644
index 000000000..2a7dffe91
--- /dev/null
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentFieldset.tsx
@@ -0,0 +1,16 @@
+import { NameField } from '../../NameField';
+
+import { PortainerUrlField } from './PortainerUrlField';
+
+interface EdgeAgentFormProps {
+ readonly?: boolean;
+}
+
+export function EdgeAgentFieldset({ readonly }: EdgeAgentFormProps) {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx
new file mode 100644
index 000000000..9eeb46b95
--- /dev/null
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx
@@ -0,0 +1,90 @@
+import { Formik, Form } from 'formik';
+
+import { LoadingButton } from '@/portainer/components/Button/LoadingButton';
+import { Environment } from '@/portainer/environments/types';
+import { useCreateEdgeAgentEnvironmentMutation } from '@/portainer/environments/queries/useCreateEnvironmentMutation';
+import { baseHref } from '@/portainer/helpers/pathHelper';
+import { FormSection } from '@/portainer/components/form-components/FormSection';
+import { EdgeCheckinIntervalField } from '@/edge/components/EdgeCheckInIntervalField';
+
+import { MoreSettingsSection } from '../../MoreSettingsSection';
+
+import { EdgeAgentFieldset } from './EdgeAgentFieldset';
+import { validationSchema } from './EdgeAgentForm.validation';
+import { FormValues } from './types';
+
+interface Props {
+ onCreate(environment: Environment): void;
+ readonly: boolean;
+}
+
+const initialValues = buildInitialValues();
+
+export function EdgeAgentForm({ onCreate, readonly }: Props) {
+ const createMutation = useCreateEdgeAgentEnvironmentMutation();
+
+ return (
+
+ initialValues={initialValues}
+ onSubmit={handleSubmit}
+ validateOnMount
+ validationSchema={validationSchema}
+ >
+ {({ isValid, setFieldValue, values }) => (
+
+ )}
+
+ );
+
+ function handleSubmit(values: typeof initialValues) {
+ createMutation.mutate(values, {
+ onSuccess(environment) {
+ onCreate(environment);
+ },
+ });
+ }
+}
+
+export function buildInitialValues(): FormValues {
+ return {
+ name: '',
+ portainerUrl: defaultPortainerUrl(),
+ pollFrequency: 0,
+ meta: {
+ groupId: 1,
+ tagIds: [],
+ },
+ };
+
+ function defaultPortainerUrl() {
+ const baseHREF = baseHref();
+ return window.location.origin + (baseHREF !== '/' ? baseHREF : '');
+ }
+}
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts
new file mode 100644
index 000000000..72bbf6984
--- /dev/null
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts
@@ -0,0 +1,16 @@
+import { number, object, SchemaOf } from 'yup';
+
+import { metadataValidation } from '../../MetadataFieldset/validation';
+import { nameValidation } from '../../NameField';
+
+import { validation as urlValidation } from './PortainerUrlField';
+import { FormValues } from './types';
+
+export function validationSchema(): SchemaOf
{
+ return object().shape({
+ name: nameValidation(),
+ portainerUrl: urlValidation(),
+ pollFrequency: number().required(),
+ meta: metadataValidation(),
+ });
+}
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/PortainerUrlField.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/PortainerUrlField.tsx
new file mode 100644
index 000000000..ce72ee93d
--- /dev/null
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/PortainerUrlField.tsx
@@ -0,0 +1,55 @@
+import { Field, useField } from 'formik';
+import { string } from 'yup';
+
+import { FormControl } from '@/portainer/components/form-components/FormControl';
+import { Input } from '@/portainer/components/form-components/Input';
+
+interface Props {
+ fieldName: string;
+ readonly?: boolean;
+}
+
+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) {
+ const [, metaProps] = useField(fieldName);
+ const id = `${fieldName}-input`;
+
+ return (
+
+
+
+ );
+}
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/index.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/index.tsx
new file mode 100644
index 000000000..7e8af3472
--- /dev/null
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/index.tsx
@@ -0,0 +1 @@
+export { EdgeAgentForm } from './EdgeAgentForm';
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/types.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/types.ts
new file mode 100644
index 000000000..6ba7356db
--- /dev/null
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/types.ts
@@ -0,0 +1,9 @@
+import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create';
+
+export interface FormValues {
+ name: string;
+
+ portainerUrl: string;
+ pollFrequency: number;
+ meta: EnvironmentMetadata;
+}
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx
new file mode 100644
index 000000000..7f00a8c15
--- /dev/null
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx
@@ -0,0 +1,66 @@
+import { v4 as uuid } from 'uuid';
+import { useReducer, useState } from 'react';
+
+import { Button } from '@/portainer/components/Button';
+import { Environment } from '@/portainer/environments/types';
+import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
+import { CommandTab } from '@/react/edge/components/EdgeScriptForm/scripts';
+import { OS, EdgeInfo } from '@/react/edge/components/EdgeScriptForm/types';
+
+import { EdgeAgentForm } from './EdgeAgentForm';
+
+interface Props {
+ onCreate: (environment: Environment) => void;
+ commands: CommandTab[] | Partial>;
+ isNomadTokenVisible?: boolean;
+}
+
+export function EdgeAgentTab({
+ onCreate,
+ commands,
+ isNomadTokenVisible,
+}: Props) {
+ const [edgeInfo, setEdgeInfo] = useState();
+
+ const [formKey, clearForm] = useReducer((state) => state + 1, 0);
+
+ return (
+ <>
+
+
+ {edgeInfo && (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )}
+ >
+ );
+
+ function handleCreate(environment: Environment) {
+ setEdgeInfo({ key: environment.EdgeKey, id: uuid() });
+ onCreate(environment);
+ }
+
+ function handleReset() {
+ setEdgeInfo(undefined);
+ clearForm();
+ }
+}
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/index.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/index.ts
new file mode 100644
index 000000000..38f4550b1
--- /dev/null
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/index.ts
@@ -0,0 +1 @@
+export { EdgeAgentTab } from './EdgeAgentTab';
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/MetadataFieldset.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/MetadataFieldset.tsx
index a21b4c458..148247d3c 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/MetadataFieldset.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/MetadataFieldset.tsx
@@ -12,7 +12,7 @@ export function MetadataFieldset() {
const { isAdmin } = useUser();
return (
-
+
) {
+ return (
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/types.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/types.ts
index 52c6fa36e..15a275156 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/types.ts
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/types.ts
@@ -1,6 +1,7 @@
export interface AnalyticsState {
dockerAgent: number;
dockerApi: number;
+ dockerEdgeAgent: number;
kubernetesAgent: number;
kubernetesEdgeAgent: number;
kaasAgent: number;
diff --git a/package.json b/package.json
index 092e13531..7306e225c 100644
--- a/package.json
+++ b/package.json
@@ -172,6 +172,7 @@
"@types/react-table": "^7.7.6",
"@types/sanitize-html": "^2.5.0",
"@types/toastr": "^2.1.39",
+ "@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.7.0",
"@typescript-eslint/parser": "^5.7.0",
"auto-ngtemplate-loader": "^2.0.1",
diff --git a/yarn.lock b/yarn.lock
index e732b86b4..31cb87651 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3565,6 +3565,11 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
+"@types/uuid@^8.3.4":
+ version "8.3.4"
+ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
+ integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
+
"@types/webpack-env@^1.16.0":
version "1.16.3"
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.3.tgz#b776327a73e561b71e7881d0cd6d34a1424db86a"