1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-09 15:55:23 +02:00

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

This commit is contained in:
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

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

View file

@ -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) {
/>
</FormControl>
<MetadataFieldset />
<MoreSettingsSection />
<div className="row">
<div className="col-sm-12">

View file

@ -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) {
<TLSFieldset />
<MetadataFieldset />
<MoreSettingsSection />
<div className="form-group">
<div className="col-sm-12">

View file

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

View file

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

View file

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

View file

@ -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) {
<OverrideSocketFieldset />
<MetadataFieldset />
<MoreSettingsSection />
<div className="form-group">
<div className="col-sm-12">
<LoadingButton

View file

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

View file

@ -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 (
<div className="form-horizontal">
@ -37,11 +67,11 @@ export function WizardDocker({ onCreate }: Props) {
</div>
</div>
{form}
{tab}
</div>
);
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 (
<EdgeAgentTab
onCreate={(environment) => onCreate(environment, 'dockerEdgeAgent')}
commands={{
linux: [commandsTabs.swarmLinux, commandsTabs.standaloneLinux],
win: [commandsTabs.swarmWindows, commandsTabs.standaloneWindow],
}}
/>
);
default:
return null;
}

View file

@ -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) {
<>
<DeploymentScripts />
<AgentForm
onCreate={(environment) => onCreate(environment, 'kubernetesAgent')}
/>
<div className="mt-5">
<AgentForm onCreate={onCreate} />
</div>
</>
);
}

View file

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

View file

@ -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<EnvironmentCreationTypes.AgentEnvironment>[] =
[
{
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 (
<div className="form-horizontal">
@ -40,16 +51,29 @@ export function WizardKubernetes({ onCreate }: Props) {
radioName="creation-type"
/>
<Component onCreate={onCreate} />
{tab}
</div>
);
}
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 (
<AgentPanel
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 { 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) {
<NameField />
<EnvironmentUrlField />
<MetadataFieldset />
<MoreSettingsSection />
<div className="form-group">
<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();
return (
<FormSection title="Metadata" isFoldable>
<FormSection title="Metadata">
<GroupField />
<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 {
dockerAgent: number;
dockerApi: number;
dockerEdgeAgent: number;
kubernetesAgent: number;
kubernetesEdgeAgent: number;
kaasAgent: number;