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 ( -
- Edge agent deployment script - - setFieldValue('os', os)} - /> - + <> {!hideIdGetter && ( <> setFieldValue(e.target.name, e.target.value)} /> - - - PORTAINER_EDGE_ID environment variable is required to - successfully connect the edge agent to Portainer - +
+
+ + PORTAINER_EDGE_ID environment variable is required + to successfully connect the edge agent to Portainer + +
+
)} + {isNomadTokenVisible && } + + + + +
- setFieldValue('allowSelfSignedCertificates', checked) + onChange={(value) => + setFieldValue('allowSelfSignedCertificates', value) } + label="Allow self-signed certs" + tooltip="When allowing self-signed certificates the edge agent will ignore the domain validation when connecting to Portainer via HTTPS" />
- - - setFieldValue(e.target.name, e.target.value)} - /> - - + ); } 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 }) => ( +
+ + + + + setFieldValue('pollFrequency', value)} + value={values.pollFrequency} + /> + + + + {!readonly && ( +
+
+ + + Create + +
+
+ )} + + )} + + ); + + 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"