feat(wizard): add edge form [EE-3000] (#6979)

pull/6958/head
Chaim Lev-Ari 2022-06-01 07:28:31 +03:00 committed by GitHub
parent e686d64011
commit ac096dda46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 793 additions and 316 deletions

View File

@ -11,6 +11,7 @@ interface Props {
isDefaultHidden?: boolean; isDefaultHidden?: boolean;
label?: string; label?: string;
tooltip?: string; tooltip?: string;
readonly?: boolean;
} }
export const checkinIntervalOptions = [ export const checkinIntervalOptions = [
@ -34,6 +35,7 @@ export const checkinIntervalOptions = [
export function EdgeCheckinIntervalField({ export function EdgeCheckinIntervalField({
value, value,
readonly,
onChange, onChange,
isDefaultHidden = false, isDefaultHidden = false,
label = 'Poll frequency', label = 'Poll frequency',
@ -49,6 +51,7 @@ export function EdgeCheckinIntervalField({
onChange(parseInt(e.currentTarget.value, 10)); onChange(parseInt(e.currentTarget.value, 10));
}} }}
options={options} options={options}
disabled={readonly}
/> />
</FormControl> </FormControl>
); );
@ -60,6 +63,7 @@ export const EdgeCheckinIntervalFieldAngular = r2a(EdgeCheckinIntervalField, [
'isDefaultHidden', 'isDefaultHidden',
'tooltip', 'tooltip',
'label', 'label',
'readonly',
]); ]);
function useOptions(isDefaultHidden: boolean) { function useOptions(isDefaultHidden: boolean) {

View File

@ -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<EdgeProperties>({
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 (
<>
<EdgePropertiesForm
setFieldValue={(key, value) =>
setEdgeProperties({ ...edgeProperties, [key]: value })
}
values={edgeProperties}
hideIdGetter={edgeId !== undefined}
/>
<ScriptTabs
values={edgeProperties}
agentVersion={agentVersion}
edgeKey={edgeKey}
onPlatformChange={(platform) =>
setEdgeProperties({ ...edgeProperties, platform })
}
edgeId={edgeId}
agentSecret={agentSecret}
/>
</>
);
}
export const EdgeScriptFormAngular = r2a(EdgeScriptForm, ['edgeKey', 'edgeId']);

View File

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

View File

@ -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;
}

View File

@ -1,9 +1,14 @@
import angular from 'angular'; import angular from 'angular';
import { r2a } from '@/react-tools/react2angular';
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
import { EdgeCheckinIntervalFieldAngular } from './EdgeCheckInIntervalField'; import { EdgeCheckinIntervalFieldAngular } from './EdgeCheckInIntervalField';
import { EdgeScriptFormAngular } from './EdgeScriptForm';
export const componentsModule = angular export const componentsModule = angular
.module('app.edge.components', []) .module('app.edge.components', [])
.component('edgeCheckinIntervalField', EdgeCheckinIntervalFieldAngular) .component(
.component('edgeScriptForm', EdgeScriptFormAngular).name; 'edgeScriptForm',
r2a(EdgeScriptForm, ['edgeInfo', 'commands', 'isNomadTokenVisible'])
)
.component('edgeCheckinIntervalField', EdgeCheckinIntervalFieldAngular).name;

View File

@ -1,5 +1,4 @@
.boxselector_wrapper { .boxselector_wrapper {
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
margin: 5px;
} }

View File

@ -5,6 +5,8 @@ import { react2angular } from '@/react-tools/react2angular';
import { BoxSelector, buildOption } from './BoxSelector'; import { BoxSelector, buildOption } from './BoxSelector';
import { BoxSelectorAngular } from './BoxSelectorAngular'; import { BoxSelectorAngular } from './BoxSelectorAngular';
export { type BoxSelectorOption } from './types';
export { BoxSelector, buildOption }; export { BoxSelector, buildOption };
const BoxSelectorReact = react2angular(BoxSelector, [ const BoxSelectorReact = react2angular(BoxSelector, [
'value', 'value',

View File

@ -60,6 +60,7 @@ export type Environment = {
TagIds: TagId[]; TagIds: TagId[];
GroupId: EnvironmentGroupId; GroupId: EnvironmentGroupId;
EdgeID?: string; EdgeID?: string;
EdgeKey: string;
EdgeCheckinInterval?: number; EdgeCheckinInterval?: number;
QueryDate?: number; QueryDate?: number;
LastCheckInDate?: number; LastCheckInDate?: number;
@ -73,7 +74,6 @@ export type Environment = {
UserTrusted: boolean; UserTrusted: boolean;
AMTDeviceGUID?: string; AMTDeviceGUID?: string;
}; };
/** /**
* TS reference of endpoint_create.go#EndpointCreationType iota * TS reference of endpoint_create.go#EndpointCreationType iota
*/ */

View File

@ -70,5 +70,6 @@ function mockEnvironment(type: EnvironmentType): Environment {
}, },
URL: 'url', URL: 'url',
UserTrusted: false, UserTrusted: false,
EdgeKey: '',
}; };
} }

View File

@ -23,6 +23,7 @@ test('loads component', async () => {
Kubernetes: { Snapshots: [] }, Kubernetes: { Snapshots: [] },
Id: 3, Id: 3,
UserTrusted: false, UserTrusted: false,
EdgeKey: '',
}; };
const { getByText } = renderComponent(env); const { getByText } = renderComponent(env);
@ -44,6 +45,7 @@ test('shows group name', async () => {
Kubernetes: { Snapshots: [] }, Kubernetes: { Snapshots: [] },
Id: 3, Id: 3,
UserTrusted: false, UserTrusted: false,
EdgeKey: '',
}; };
const { findByText } = renderComponent(env, { Name: groupName }); const { findByText } = renderComponent(env, { Name: groupName });

View File

@ -2,12 +2,23 @@ import { useMutation } from 'react-query';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget'; import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget';
import { EdgeScriptForm } from '@/edge/components/EdgeScriptForm';
import { generateKey } from '@/portainer/environments/environment.service/edge'; import { generateKey } from '@/portainer/environments/environment.service/edge';
import { useSettings } from '@/portainer/settings/queries'; 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'; import { AutoEnvCreationSettingsForm } from './AutoEnvCreationSettingsForm';
const commands = {
linux: [
commandsTabs.k8sLinux,
commandsTabs.swarmLinux,
commandsTabs.standaloneLinux,
commandsTabs.nomadLinux,
],
win: [commandsTabs.swarmWindows, commandsTabs.standaloneWindow],
};
export function AutomaticEdgeEnvCreation() { export function AutomaticEdgeEnvCreation() {
const edgeKeyMutation = useGenerateKeyMutation(); const edgeKeyMutation = useGenerateKeyMutation();
const { mutate: generateKey } = edgeKeyMutation; const { mutate: generateKey } = edgeKeyMutation;
@ -39,7 +50,13 @@ export function AutomaticEdgeEnvCreation() {
{edgeKeyMutation.isLoading ? ( {edgeKeyMutation.isLoading ? (
<div>Generating key for {url} ... </div> <div>Generating key for {url} ... </div>
) : ( ) : (
edgeKey && <EdgeScriptForm edgeKey={edgeKey} /> edgeKey && (
<EdgeScriptForm
edgeInfo={{ key: edgeKey }}
commands={commands}
isNomadTokenVisible
/>
)
)} )}
</WidgetBody> </WidgetBody>
</Widget> </Widget>

View File

@ -54,7 +54,12 @@
</p> </p>
</span> </span>
<edge-script-form edge-key="endpoint.EdgeKey" edge-id="endpoint.EdgeID"></edge-script-form> <div class="col-sm-12 form-section-title"> Edge agent deployment script </div>
<edge-script-form
edge-info="{ key: endpoint.EdgeKey, id: endpoint.EdgeID }"
commands="state.edgeScriptCommands"
is-nomad-token-visible="state.showNomad"
></edge-script-form>
<span class="small text-muted"> <span class="small text-muted">
<div class="col-sm-12 form-section-title" style="margin-top: 25px"> Join token </div> <div class="col-sm-12 form-section-title" style="margin-top: 25px"> Join token </div>

View File

@ -8,6 +8,8 @@ import { getAMTInfo } from 'Portainer/hostmanagement/open-amt/open-amt.service';
import { confirmAsync } from '@/portainer/services/modal.service/confirm'; import { confirmAsync } from '@/portainer/services/modal.service/confirm';
import { isEdgeEnvironment } from '@/portainer/environments/utils'; import { isEdgeEnvironment } from '@/portainer/environments/utils';
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
angular.module('portainer.app').controller('EndpointController', EndpointController); angular.module('portainer.app').controller('EndpointController', EndpointController);
/* @ngInject */ /* @ngInject */
@ -29,6 +31,7 @@ function EndpointController(
$scope.onChangeCheckInInterval = onChangeCheckInInterval; $scope.onChangeCheckInInterval = onChangeCheckInInterval;
$scope.setFieldValue = setFieldValue; $scope.setFieldValue = setFieldValue;
$scope.onChangeTags = onChangeTags; $scope.onChangeTags = onChangeTags;
const isBE = process.env.PORTAINER_EDITION === 'BE';
$scope.state = { $scope.state = {
uploadInProgress: false, uploadInProgress: false,
@ -41,6 +44,11 @@ function EndpointController(
allowCreate: Authentication.isAdmin(), allowCreate: Authentication.isAdmin(),
allowSelfSignedCerts: true, allowSelfSignedCerts: true,
showAMTInfo: false, showAMTInfo: false,
showNomad: isBE,
edgeScriptCommands: {
linux: _.compact([commandsTabs.k8sLinux, commandsTabs.swarmLinux, commandsTabs.standaloneLinux, isBE && commandsTabs.nomadLinux]),
win: [commandsTabs.swarmWindows, commandsTabs.standaloneWindow],
},
}; };
$scope.formValues = { $scope.formValues = {

View File

@ -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<Record<OS, CommandTab[]>>;
isNomadTokenVisible?: boolean;
}
export function EdgeScriptForm({
edgeInfo,
commands,
isNomadTokenVisible,
}: Props) {
const showOsSelector = !(commands instanceof Array);
return (
<div className="form-horizontal">
<Formik
initialValues={edgePropertiesFormInitialValues}
validationSchema={() => validationSchema(isNomadTokenVisible)}
onSubmit={() => {}}
>
{({ values, setFieldValue }) => (
<>
<EdgeScriptSettingsFieldset
isNomadTokenVisible={
isNomadTokenVisible && values.platform === 'nomad'
}
hideIdGetter={edgeInfo.id !== undefined}
/>
<div className="mt-8">
{showOsSelector && (
<OsSelector
value={values.os}
onChange={(value) => setFieldValue('os', value)}
/>
)}
<ScriptTabs
edgeId={edgeInfo.id}
edgeKey={edgeInfo.key}
values={values}
commands={showOsSelector ? commands[values.os] || [] : commands}
platform={values.platform}
onPlatformChange={(platform) =>
setFieldValue('platform', platform)
}
/>
</div>
</>
)}
</Formik>
</div>
);
}

View File

@ -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() : {}),
});
}

View File

@ -1,32 +1,26 @@
import { useFormikContext, Field } from 'formik';
import { FormControl } from '@/portainer/components/form-components/FormControl'; import { FormControl } from '@/portainer/components/form-components/FormControl';
import { Input } from '@/portainer/components/form-components/Input'; 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 { SwitchField } from '@/portainer/components/form-components/SwitchField';
import { TextTip } from '@/portainer/components/Tip/TextTip'; import { TextTip } from '@/portainer/components/Tip/TextTip';
import { OsSelector } from './OsSelector'; import { NomadTokenField } from './NomadTokenField';
import { EdgeProperties } from './types'; import { ScriptFormValues } from './types';
interface Props { interface Props {
setFieldValue<T>(key: string, value: T): void; isNomadTokenVisible?: boolean;
values: EdgeProperties; hideIdGetter?: boolean;
hideIdGetter: boolean;
} }
export function EdgePropertiesForm({ export function EdgeScriptSettingsFieldset({
setFieldValue, isNomadTokenVisible,
values,
hideIdGetter, hideIdGetter,
}: Props) { }: Props) {
const { values, setFieldValue } = useFormikContext<ScriptFormValues>();
return ( return (
<form className="form-horizontal"> <>
<FormSectionTitle>Edge agent deployment script</FormSectionTitle>
<OsSelector
value={values.os}
onChange={(os) => setFieldValue('os', os)}
/>
{!hideIdGetter && ( {!hideIdGetter && (
<> <>
<FormControl <FormControl
@ -42,40 +36,44 @@ export function EdgePropertiesForm({
onChange={(e) => setFieldValue(e.target.name, e.target.value)} onChange={(e) => setFieldValue(e.target.name, e.target.value)}
/> />
</FormControl> </FormControl>
<div className="form-group">
<TextTip color="blue"> <div className="col-sm-12">
<code>PORTAINER_EDGE_ID</code> environment variable is required to <TextTip color="blue">
successfully connect the edge agent to Portainer <code>PORTAINER_EDGE_ID</code> environment variable is required
</TextTip> to successfully connect the edge agent to Portainer
</TextTip>
</div>
</div>
</> </>
)} )}
{isNomadTokenVisible && <NomadTokenField />}
<FormControl
label="Environment variables"
tooltip="Comma separated list of environment variables that will be sourced from the host where the agent is deployed."
inputId="env-variables-input"
>
<Field
name="envVars"
as={Input}
placeholder="foo=bar,myvar"
id="env-variables-input"
/>
</FormControl>
<div className="form-group"> <div className="form-group">
<div className="col-sm-12"> <div className="col-sm-12">
<SwitchField <SwitchField
checked={values.allowSelfSignedCertificates} checked={values.allowSelfSignedCertificates}
label="Allow self-signed certificates" onChange={(value) =>
tooltip="When allowing self-signed certificates the edge agent will ignore the domain validation when connecting to Portainer via HTTPS" setFieldValue('allowSelfSignedCertificates', value)
onChange={(checked) =>
setFieldValue('allowSelfSignedCertificates', checked)
} }
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"
/> />
</div> </div>
</div> </div>
</>
<FormControl
label="Environment variables"
tooltip="Comma separated list of environment variables that will be sourced from the host where the agent is deployed."
inputId="env-vars-input"
>
<Input
type="text"
name="envVars"
value={values.envVars}
id="env-vars-input"
onChange={(e) => setFieldValue(e.target.name, e.target.value)}
/>
</FormControl>
</form>
); );
} }

View File

@ -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<ScriptFormValues>();
return (
<>
<div className="form-group">
<div className="col-sm-12">
<SwitchField
checked={values.authEnabled}
onChange={(value) => {
if (!value) {
setFieldValue('nomadToken', '');
}
setFieldValue('authEnabled', value);
}}
label="Nomad Authentication Enabled"
tooltip="Nomad authentication is only required if you have ACL enabled"
/>
</div>
</div>
{values.authEnabled && (
<FormControl
label="Nomad Token"
inputId="nomad-token-input"
errors={errors.nomadToken}
>
<Field name="nomadToken" as={Input} id="nomad-token-input" />
</FormControl>
)}
</>
);
}
export function validation() {
return {
nomadToken: string().when('authEnabled', {
is: true,
then: string().required('Token is required'),
}),
authEnabled: boolean(),
};
}

View File

@ -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: (
<>
<Code>{cmd}</Code>
<CopyButton copyText={cmd}>Copy</CopyButton>
</>
),
};
});
return (
<div className="row">
<div className="col-sm-12">
<NavTabs
selectedId={platform}
options={options}
onSelect={(id: Platform) => onPlatformChange(id)}
/>
</div>
</div>
);
}

View File

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

View File

@ -1,111 +1,50 @@
import { useEffect } from 'react';
import _ from 'lodash'; 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 { getAgentShortVersion } from '@/portainer/views/endpoints/helpers';
import { EdgeProperties, Platform } from './types'; import { ScriptFormValues, Platform } from './types';
const commandsByOs = { type CommandGenerator = (
linux: [ agentVersion: string,
{ edgeKey: string,
id: 'k8s', properties: ScriptFormValues,
label: 'Kubernetes', edgeId?: string,
command: buildKubernetesCommand, agentSecret?: string
}, ) => string;
{
id: 'swarm', export type CommandTab = {
label: 'Docker Swarm', id: Platform;
command: buildLinuxSwarmCommand, label: string;
}, command: CommandGenerator;
{
id: 'standalone',
label: 'Docker Standalone',
command: buildLinuxStandaloneCommand,
},
],
win: [
{
id: 'swarm',
label: 'Docker Swarm',
command: buildWindowsSwarmCommand,
},
{
id: 'standalone',
label: 'Docker Standalone',
command: buildWindowsStandaloneCommand,
},
],
}; };
interface Props { export const commandsTabs: Record<string, CommandTab> = {
values: EdgeProperties; k8sLinux: {
edgeKey: string; id: 'k8s',
agentVersion: string; label: 'Kubernetes',
edgeId?: string; command: buildLinuxKubernetesCommand,
agentSecret?: string; },
onPlatformChange(platform: Platform): void; swarmLinux: {
} id: 'swarm',
label: 'Docker Swarm',
export function ScriptTabs({ command: buildLinuxSwarmCommand,
agentVersion, },
values, standaloneLinux: {
edgeKey, id: 'standalone',
edgeId, label: 'Docker Standalone',
agentSecret, command: buildLinuxStandaloneCommand,
onPlatformChange, },
}: Props) { swarmWindows: {
const { id: 'swarm',
os, label: 'Docker Swarm',
allowSelfSignedCertificates, command: buildWindowsSwarmCommand,
edgeIdGenerator, },
envVars, standaloneWindow: {
platform, id: 'standalone',
} = values; label: 'Docker Standalone',
command: buildWindowsStandaloneCommand,
useEffect(() => { },
if (!commandsByOs[os].find((p) => p.id === platform)) { } as const;
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: (
<>
<Code>{cmd}</Code>
<CopyButton copyText={cmd}>Copy</CopyButton>
</>
),
};
});
return (
<div className="row">
<div className="col-sm-12">
<NavTabs
selectedId={platform}
options={options}
onSelect={(id: Platform) => onPlatformChange(id)}
/>
</div>
</div>
);
}
function buildDockerEnvVars(envVars: string, defaultVars: string[]) { function buildDockerEnvVars(envVars: string, defaultVars: string[]) {
const vars = defaultVars.concat( const vars = defaultVars.concat(
@ -115,26 +54,28 @@ function buildDockerEnvVars(envVars: string, defaultVars: string[]) {
return vars.map((s) => `-e ${s}`).join(' \\\n '); return vars.map((s) => `-e ${s}`).join(' \\\n ');
} }
function buildLinuxStandaloneCommand( export function buildLinuxStandaloneCommand(
agentVersion: string, agentVersion: string,
edgeIdScript: string,
edgeKey: string, edgeKey: string,
allowSelfSignedCerts: boolean, properties: ScriptFormValues,
envVars: string,
edgeId?: string, edgeId?: string,
agentSecret?: string agentSecret?: string
) { ) {
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
const env = buildDockerEnvVars( const env = buildDockerEnvVars(
envVars, envVars,
buildDefaultEnvVars( buildDefaultEnvVars(
edgeKey, edgeKey,
allowSelfSignedCerts, allowSelfSignedCertificates,
!edgeIdScript ? edgeId : undefined, !edgeIdGenerator ? edgeId : undefined,
agentSecret agentSecret
) )
); );
return `${edgeIdScript ? `PORTAINER_EDGE_ID=$(${edgeIdScript}) \n\n` : ''}\ return `${
edgeIdGenerator ? `PORTAINER_EDGE_ID=$(${edgeIdGenerator}) \n\n` : ''
}\
docker run -d \\ docker run -d \\
-v /var/run/docker.sock:/var/run/docker.sock \\ -v /var/run/docker.sock:/var/run/docker.sock \\
-v /var/lib/docker/volumes:/var/lib/docker/volumes \\ -v /var/lib/docker/volumes:/var/lib/docker/volumes \\
@ -147,27 +88,29 @@ docker run -d \\
`; `;
} }
function buildWindowsStandaloneCommand( export function buildWindowsStandaloneCommand(
agentVersion: string, agentVersion: string,
edgeIdScript: string,
edgeKey: string, edgeKey: string,
allowSelfSignedCerts: boolean, properties: ScriptFormValues,
envVars: string,
edgeId?: string, edgeId?: string,
agentSecret?: string agentSecret?: string
) { ) {
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
const env = buildDockerEnvVars( const env = buildDockerEnvVars(
envVars, envVars,
buildDefaultEnvVars( buildDefaultEnvVars(
edgeKey, edgeKey,
allowSelfSignedCerts, allowSelfSignedCertificates,
edgeIdScript ? '$Env:PORTAINER_EDGE_ID' : edgeId, edgeIdGenerator ? '$Env:PORTAINER_EDGE_ID' : edgeId,
agentSecret agentSecret
) )
); );
return `${ return `${
edgeIdScript ? `$Env:PORTAINER_EDGE_ID = "@(${edgeIdScript})" \n\n` : '' edgeIdGenerator
? `$Env:PORTAINER_EDGE_ID = "@(${edgeIdGenerator})" \n\n`
: ''
}\ }\
docker run -d \\ docker run -d \\
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\ --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, agentVersion: string,
edgeIdScript: string,
edgeKey: string, edgeKey: string,
allowSelfSignedCerts: boolean, properties: ScriptFormValues,
envVars: string,
edgeId?: string, edgeId?: string,
agentSecret?: string agentSecret?: string
) { ) {
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
const env = buildDockerEnvVars(envVars, [ const env = buildDockerEnvVars(envVars, [
...buildDefaultEnvVars( ...buildDefaultEnvVars(
edgeKey, edgeKey,
allowSelfSignedCerts, allowSelfSignedCertificates,
!edgeIdScript ? edgeId : undefined, !edgeIdGenerator ? edgeId : undefined,
agentSecret agentSecret
), ),
'AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent', '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 \\ docker network create \\
--driver overlay \\ --driver overlay \\
portainer_agent_network; portainer_agent_network;
docker service create \\ docker service create \\
--name portainer_edge_agent \\ --name portainer_edge_agent \\
--network portainer_agent_network \\ --network portainer_agent_network \\
@ -218,28 +163,30 @@ docker service create \\
`; `;
} }
function buildWindowsSwarmCommand( export function buildWindowsSwarmCommand(
agentVersion: string, agentVersion: string,
edgeIdScript: string,
edgeKey: string, edgeKey: string,
allowSelfSignedCerts: boolean, properties: ScriptFormValues,
envVars: string,
edgeId?: string, edgeId?: string,
agentSecret?: string agentSecret?: string
) { ) {
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
const env = buildDockerEnvVars(envVars, [ const env = buildDockerEnvVars(envVars, [
...buildDefaultEnvVars( ...buildDefaultEnvVars(
edgeKey, edgeKey,
allowSelfSignedCerts, allowSelfSignedCertificates,
edgeIdScript ? '$Env:PORTAINER_EDGE_ID' : edgeId, edgeIdGenerator ? '$Env:PORTAINER_EDGE_ID' : edgeId,
agentSecret agentSecret
), ),
'AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent', 'AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent',
]); ]);
return `${ return `${
edgeIdScript ? `$Env:PORTAINER_EDGE_ID = "@(${edgeIdScript})" \n\n` : '' edgeIdGenerator
} ? `$Env:PORTAINER_EDGE_ID = "@(${edgeIdGenerator})" \n\n`
: ''
}\
docker network create \\ docker network create \\
--driver overlay \\ --driver overlay \\
portainer_agent_network; portainer_agent_network;
@ -257,24 +204,24 @@ docker service create \\
`; `;
} }
function buildKubernetesCommand( export function buildLinuxKubernetesCommand(
agentVersion: string, agentVersion: string,
edgeIdScript: string,
edgeKey: string, edgeKey: string,
allowSelfSignedCerts: boolean, properties: ScriptFormValues,
envVars: string,
edgeId?: string, edgeId?: string,
agentSecret = '' agentSecret?: string
) { ) {
const agentShortVersion = getAgentShortVersion(agentVersion); const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
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';
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( function buildDefaultEnvVars(

View File

@ -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;
}

View File

@ -193,6 +193,7 @@ function useAnalyticsState() {
aciApi: 0, aciApi: 0,
localEndpoint: 0, localEndpoint: 0,
nomadEdgeAgent: 0, nomadEdgeAgent: 0,
dockerEdgeAgent: 0,
}); });
return { analytics, setAnalytics }; return { analytics, setAnalytics };

View File

@ -13,8 +13,8 @@ import { EnvironmentMetadata } from '@/portainer/environments/environment.servic
import { NameField, nameValidation } from '../shared/NameField'; import { NameField, nameValidation } from '../shared/NameField';
import { AnalyticsStateKey } from '../types'; import { AnalyticsStateKey } from '../types';
import { MetadataFieldset } from '../shared/MetadataFieldset';
import { metadataValidation } from '../shared/MetadataFieldset/validation'; import { metadataValidation } from '../shared/MetadataFieldset/validation';
import { MoreSettingsSection } from '../shared/MoreSettingsSection';
interface FormValues { interface FormValues {
name: string; name: string;
@ -109,7 +109,7 @@ export function WizardAzure({ onCreate }: Props) {
/> />
</FormControl> </FormControl>
<MetadataFieldset /> <MoreSettingsSection />
<div className="row"> <div className="row">
<div className="col-sm-12"> <div className="col-sm-12">

View File

@ -12,7 +12,7 @@ import {
} from '@/portainer/environments/types'; } from '@/portainer/environments/types';
import { NameField } from '../../shared/NameField'; import { NameField } from '../../shared/NameField';
import { MetadataFieldset } from '../../shared/MetadataFieldset'; import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
import { validation } from './APIForm.validation'; import { validation } from './APIForm.validation';
import { FormValues } from './types'; import { FormValues } from './types';
@ -66,7 +66,7 @@ export function APIForm({ onCreate }: Props) {
<TLSFieldset /> <TLSFieldset />
<MetadataFieldset /> <MoreSettingsSection />
<div className="form-group"> <div className="form-group">
<div className="col-sm-12"> <div className="col-sm-12">

View File

@ -12,7 +12,7 @@ export function APITab({ onCreate }: Props) {
<> <>
<DeploymentScripts /> <DeploymentScripts />
<div className="wizard-form"> <div className="mt-5">
<APIForm onCreate={onCreate} /> <APIForm onCreate={onCreate} />
</div> </div>
</> </>

View File

@ -55,9 +55,7 @@ function DeployCode({ code }: DeployCodeProps) {
</span> </span>
<Code>{code}</Code> <Code>{code}</Code>
<CopyButton copyText={code} className="my-6"> <CopyButton copyText={code}>Copy command</CopyButton>
Copy command
</CopyButton>
</> </>
); );
} }

View File

@ -13,7 +13,7 @@ export function AgentTab({ onCreate }: Props) {
<> <>
<DeploymentScripts /> <DeploymentScripts />
<div className="wizard-form"> <div className="mt-5">
<AgentForm onCreate={onCreate} /> <AgentForm onCreate={onCreate} />
</div> </div>
</> </>

View File

@ -10,7 +10,7 @@ import { SwitchField } from '@/portainer/components/form-components/SwitchField'
import { Environment } from '@/portainer/environments/types'; import { Environment } from '@/portainer/environments/types';
import { NameField } from '../../shared/NameField'; import { NameField } from '../../shared/NameField';
import { MetadataFieldset } from '../../shared/MetadataFieldset'; import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
import { validation } from './SocketForm.validation'; import { validation } from './SocketForm.validation';
import { FormValues } from './types'; import { FormValues } from './types';
@ -44,8 +44,7 @@ export function SocketForm({ onCreate }: Props) {
<OverrideSocketFieldset /> <OverrideSocketFieldset />
<MetadataFieldset /> <MoreSettingsSection />
<div className="form-group"> <div className="form-group">
<div className="col-sm-12"> <div className="col-sm-12">
<LoadingButton <LoadingButton

View File

@ -13,7 +13,7 @@ export function SocketTab({ onCreate }: Props) {
<> <>
<DeploymentScripts /> <DeploymentScripts />
<div className="wizard-form"> <div className="mt-5">
<SocketForm onCreate={onCreate} /> <SocketForm onCreate={onCreate} />
</div> </div>
</> </>

View File

@ -1,9 +1,14 @@
import { useState } from 'react'; 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 { Environment } from '@/portainer/environments/types';
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
import { AnalyticsStateKey } from '../types'; import { AnalyticsStateKey } from '../types';
import { EdgeAgentTab } from '../shared/EdgeAgentTab';
import { AgentTab } from './AgentTab'; import { AgentTab } from './AgentTab';
import { APITab } from './APITab'; import { APITab } from './APITab';
@ -13,16 +18,41 @@ interface Props {
onCreate(environment: Environment, analytics: AnalyticsStateKey): void; onCreate(environment: Environment, analytics: AnalyticsStateKey): void;
} }
const options = [ const options: BoxSelectorOption<'agent' | 'api' | 'socket' | 'edgeAgent'>[] = [
buildOption('Agent', 'fa fa-bolt', 'Agent', '', 'agent'), {
buildOption('API', 'fa fa-cloud', 'API', '', 'api'), id: 'agent',
buildOption('Socket', 'fab fa-docker', 'Socket', '', 'socket'), 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) { export function WizardDocker({ onCreate }: Props) {
const [creationType, setCreationType] = useState(options[0].value); const [creationType, setCreationType] = useState(options[0].value);
const form = getForm(creationType); const tab = getTab(creationType);
return ( return (
<div className="form-horizontal"> <div className="form-horizontal">
@ -37,11 +67,11 @@ export function WizardDocker({ onCreate }: Props) {
</div> </div>
</div> </div>
{form} {tab}
</div> </div>
); );
function getForm(creationType: 'agent' | 'api' | 'socket') { function getTab(creationType: 'agent' | 'api' | 'socket' | 'edgeAgent') {
switch (creationType) { switch (creationType) {
case 'agent': case 'agent':
return ( return (
@ -61,6 +91,16 @@ export function WizardDocker({ onCreate }: Props) {
onCreate={(environment) => onCreate(environment, 'localEndpoint')} onCreate={(environment) => onCreate(environment, 'localEndpoint')}
/> />
); );
case 'edgeAgent':
return (
<EdgeAgentTab
onCreate={(environment) => onCreate(environment, 'dockerEdgeAgent')}
commands={{
linux: [commandsTabs.swarmLinux, commandsTabs.standaloneLinux],
win: [commandsTabs.swarmWindows, commandsTabs.standaloneWindow],
}}
/>
);
default: default:
return null; return null;
} }

View File

@ -1,12 +1,11 @@
import { Environment } from '@/portainer/environments/types'; import { Environment } from '@/portainer/environments/types';
import { AgentForm } from '../shared/AgentForm/AgentForm'; import { AgentForm } from '../shared/AgentForm';
import { AnalyticsStateKey } from '../types';
import { DeploymentScripts } from './DeploymentScripts'; import { DeploymentScripts } from './DeploymentScripts';
interface Props { interface Props {
onCreate(environment: Environment, analytics: AnalyticsStateKey): void; onCreate(environment: Environment): void;
} }
export function AgentPanel({ onCreate }: Props) { export function AgentPanel({ onCreate }: Props) {
@ -14,9 +13,9 @@ export function AgentPanel({ onCreate }: Props) {
<> <>
<DeploymentScripts /> <DeploymentScripts />
<AgentForm <div className="mt-5">
onCreate={(environment) => onCreate(environment, 'kubernetesAgent')} <AgentForm onCreate={onCreate} />
/> </div>
</> </>
); );
} }

View File

@ -106,9 +106,7 @@ function DeployCode({
</p> </p>
)} )}
<Code>{code}</Code> <Code>{code}</Code>
<CopyButton copyText={code} className="my-6"> <CopyButton copyText={code}>Copy command</CopyButton>
Copy command
</CopyButton>
</> </>
); );
} }

View File

@ -6,8 +6,10 @@ import {
EnvironmentCreationTypes, EnvironmentCreationTypes,
} from '@/portainer/environments/types'; } from '@/portainer/environments/types';
import { BoxSelectorOption } from '@/portainer/components/BoxSelector/types'; import { BoxSelectorOption } from '@/portainer/components/BoxSelector/types';
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
import { AnalyticsStateKey } from '../types'; import { AnalyticsStateKey } from '../types';
import { EdgeAgentTab } from '../shared/EdgeAgentTab';
import { AgentPanel } from './AgentPanel'; import { AgentPanel } from './AgentPanel';
@ -15,21 +17,30 @@ interface Props {
onCreate(environment: Environment, analytics: AnalyticsStateKey): void; onCreate(environment: Environment, analytics: AnalyticsStateKey): void;
} }
const options: BoxSelectorOption<EnvironmentCreationTypes.AgentEnvironment>[] = const options: BoxSelectorOption<
[ | EnvironmentCreationTypes.AgentEnvironment
{ | EnvironmentCreationTypes.EdgeAgentEnvironment
id: 'agent_endpoint', >[] = [
icon: 'fa fa-bolt', {
label: 'Agent', id: 'agent_endpoint',
value: EnvironmentCreationTypes.AgentEnvironment, icon: 'fa fa-bolt',
description: '', 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) { export function WizardKubernetes({ onCreate }: Props) {
const [creationType, setCreationType] = useState(options[0].value); const [creationType, setCreationType] = useState(options[0].value);
const Component = getPanel(creationType); const tab = getTab(creationType);
return ( return (
<div className="form-horizontal"> <div className="form-horizontal">
@ -40,16 +51,29 @@ export function WizardKubernetes({ onCreate }: Props) {
radioName="creation-type" radioName="creation-type"
/> />
<Component onCreate={onCreate} /> {tab}
</div> </div>
); );
}
function getPanel(type: typeof options[number]['value']) { function getTab(type: typeof options[number]['value']) {
switch (type) { switch (type) {
case EnvironmentCreationTypes.AgentEnvironment: case EnvironmentCreationTypes.AgentEnvironment:
return AgentPanel; return (
default: <AgentPanel
throw new Error('Creation type not supported'); onCreate={(environment) => onCreate(environment, 'kubernetesAgent')}
/>
);
case EnvironmentCreationTypes.EdgeAgentEnvironment:
return (
<EdgeAgentTab
onCreate={(environment) =>
onCreate(environment, 'kubernetesEdgeAgent')
}
commands={[{ ...commandsTabs.k8sLinux, label: 'Linux' }]}
/>
);
default:
throw new Error('Creation type not supported');
}
} }
} }

View File

@ -8,7 +8,7 @@ import { Environment } from '@/portainer/environments/types';
import { CreateAgentEnvironmentValues } from '@/portainer/environments/environment.service/create'; import { CreateAgentEnvironmentValues } from '@/portainer/environments/environment.service/create';
import { NameField } from '../NameField'; import { NameField } from '../NameField';
import { MetadataFieldset } from '../MetadataFieldset'; import { MoreSettingsSection } from '../MoreSettingsSection';
import { EnvironmentUrlField } from './EnvironmentUrlField'; import { EnvironmentUrlField } from './EnvironmentUrlField';
import { validation } from './AgentForm.validation'; import { validation } from './AgentForm.validation';
@ -44,7 +44,7 @@ export function AgentForm({ onCreate }: Props) {
<NameField /> <NameField />
<EnvironmentUrlField /> <EnvironmentUrlField />
<MetadataFieldset /> <MoreSettingsSection />
<div className="form-group"> <div className="form-group">
<div className="col-sm-12"> <div className="col-sm-12">

View File

@ -0,0 +1,16 @@
import { NameField } from '../../NameField';
import { PortainerUrlField } from './PortainerUrlField';
interface EdgeAgentFormProps {
readonly?: boolean;
}
export function EdgeAgentFieldset({ readonly }: EdgeAgentFormProps) {
return (
<>
<NameField readonly={readonly} />
<PortainerUrlField fieldName="portainerUrl" readonly={readonly} />
</>
);
}

View File

@ -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 (
<Formik<FormValues>
initialValues={initialValues}
onSubmit={handleSubmit}
validateOnMount
validationSchema={validationSchema}
>
{({ isValid, setFieldValue, values }) => (
<Form>
<EdgeAgentFieldset readonly={readonly} />
<MoreSettingsSection>
<FormSection title="Check-in Intervals">
<EdgeCheckinIntervalField
readonly={readonly}
onChange={(value) => setFieldValue('pollFrequency', value)}
value={values.pollFrequency}
/>
</FormSection>
</MoreSettingsSection>
{!readonly && (
<div className="row">
<div className="col-sm-12">
<LoadingButton
isLoading={createMutation.isLoading}
loadingText="Creating environment..."
disabled={!isValid}
>
<i className="fa fa-plug space-right" />
Create
</LoadingButton>
</div>
</div>
)}
</Form>
)}
</Formik>
);
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 : '');
}
}

View File

@ -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<FormValues> {
return object().shape({
name: nameValidation(),
portainerUrl: urlValidation(),
pollFrequency: number().required(),
meta: metadataValidation(),
});
}

View File

@ -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 (
<FormControl
label="Portainer server URL"
tooltip="URL of the Portainer instance that the agent will use to initiate the communications."
required
errors={metaProps.error}
inputId={id}
>
<Field
id={id}
name={fieldName}
as={Input}
placeholder="e.g. 10.0.0.10:9443 or portainer.mydomain.com"
required
data-cy="endpointCreate-portainerServerUrlInput"
readOnly={readonly}
/>
</FormControl>
);
}

View File

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

View File

@ -0,0 +1,9 @@
import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create';
export interface FormValues {
name: string;
portainerUrl: string;
pollFrequency: number;
meta: EnvironmentMetadata;
}

View File

@ -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<Record<OS, CommandTab[]>>;
isNomadTokenVisible?: boolean;
}
export function EdgeAgentTab({
onCreate,
commands,
isNomadTokenVisible,
}: Props) {
const [edgeInfo, setEdgeInfo] = useState<EdgeInfo>();
const [formKey, clearForm] = useReducer((state) => state + 1, 0);
return (
<>
<EdgeAgentForm
onCreate={handleCreate}
readonly={!!edgeInfo}
key={formKey}
/>
{edgeInfo && (
<>
<EdgeScriptForm
edgeInfo={edgeInfo}
commands={commands}
isNomadTokenVisible={isNomadTokenVisible}
/>
<hr />
<div className="row">
<div className="flex justify-end">
<Button color="primary" type="reset" onClick={handleReset}>
Add another environment
</Button>
</div>
</div>
</>
)}
</>
);
function handleCreate(environment: Environment) {
setEdgeInfo({ key: environment.EdgeKey, id: uuid() });
onCreate(environment);
}
function handleReset() {
setEdgeInfo(undefined);
clearForm();
}
}

View File

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

View File

@ -12,7 +12,7 @@ export function MetadataFieldset() {
const { isAdmin } = useUser(); const { isAdmin } = useUser();
return ( return (
<FormSection title="Metadata" isFoldable> <FormSection title="Metadata">
<GroupField /> <GroupField />
<TagSelector <TagSelector

View File

@ -0,0 +1,17 @@
import { PropsWithChildren } from 'react';
import { FormSection } from '@/portainer/components/form-components/FormSection';
import { MetadataFieldset } from './MetadataFieldset';
export function MoreSettingsSection({ children }: PropsWithChildren<unknown>) {
return (
<FormSection title="More settings" isFoldable>
<div className="ml-8">
{children}
<MetadataFieldset />
</div>
</FormSection>
);
}

View File

@ -1,6 +1,7 @@
export interface AnalyticsState { export interface AnalyticsState {
dockerAgent: number; dockerAgent: number;
dockerApi: number; dockerApi: number;
dockerEdgeAgent: number;
kubernetesAgent: number; kubernetesAgent: number;
kubernetesEdgeAgent: number; kubernetesEdgeAgent: number;
kaasAgent: number; kaasAgent: number;

View File

@ -172,6 +172,7 @@
"@types/react-table": "^7.7.6", "@types/react-table": "^7.7.6",
"@types/sanitize-html": "^2.5.0", "@types/sanitize-html": "^2.5.0",
"@types/toastr": "^2.1.39", "@types/toastr": "^2.1.39",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.7.0", "@typescript-eslint/eslint-plugin": "^5.7.0",
"@typescript-eslint/parser": "^5.7.0", "@typescript-eslint/parser": "^5.7.0",
"auto-ngtemplate-loader": "^2.0.1", "auto-ngtemplate-loader": "^2.0.1",

View File

@ -3565,6 +3565,11 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== 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": "@types/webpack-env@^1.16.0":
version "1.16.3" version "1.16.3"
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.3.tgz#b776327a73e561b71e7881d0cd6d34a1424db86a" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.3.tgz#b776327a73e561b71e7881d0cd6d34a1424db86a"