mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
feat(wizard): add edge form [EE-3000] (#6979)
This commit is contained in:
parent
e686d64011
commit
ac096dda46
48 changed files with 793 additions and 316 deletions
70
app/react/edge/components/EdgeScriptForm/EdgeScriptForm.tsx
Normal file
70
app/react/edge/components/EdgeScriptForm/EdgeScriptForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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() : {}),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
import { useFormikContext, Field } from 'formik';
|
||||
|
||||
import { FormControl } from '@/portainer/components/form-components/FormControl';
|
||||
import { Input } from '@/portainer/components/form-components/Input';
|
||||
import { SwitchField } from '@/portainer/components/form-components/SwitchField';
|
||||
import { TextTip } from '@/portainer/components/Tip/TextTip';
|
||||
|
||||
import { NomadTokenField } from './NomadTokenField';
|
||||
import { ScriptFormValues } from './types';
|
||||
|
||||
interface Props {
|
||||
isNomadTokenVisible?: boolean;
|
||||
hideIdGetter?: boolean;
|
||||
}
|
||||
|
||||
export function EdgeScriptSettingsFieldset({
|
||||
isNomadTokenVisible,
|
||||
hideIdGetter,
|
||||
}: Props) {
|
||||
const { values, setFieldValue } = useFormikContext<ScriptFormValues>();
|
||||
|
||||
return (
|
||||
<>
|
||||
{!hideIdGetter && (
|
||||
<>
|
||||
<FormControl
|
||||
label="Edge ID Generator"
|
||||
tooltip="A bash script one liner that will generate the edge id and will be assigned to the PORTAINER_EDGE_ID environment variable"
|
||||
inputId="edge-id-generator-input"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
name="edgeIdGenerator"
|
||||
value={values.edgeIdGenerator}
|
||||
id="edge-id-generator-input"
|
||||
onChange={(e) => setFieldValue(e.target.name, e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<TextTip color="blue">
|
||||
<code>PORTAINER_EDGE_ID</code> environment variable is required
|
||||
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="col-sm-12">
|
||||
<SwitchField
|
||||
checked={values.allowSelfSignedCertificates}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
53
app/react/edge/components/EdgeScriptForm/NomadTokenField.tsx
Normal file
53
app/react/edge/components/EdgeScriptForm/NomadTokenField.tsx
Normal 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(),
|
||||
};
|
||||
}
|
45
app/react/edge/components/EdgeScriptForm/OsSelector.tsx
Normal file
45
app/react/edge/components/EdgeScriptForm/OsSelector.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { ButtonSelector } from '@/portainer/components/form-components/ButtonSelector/ButtonSelector';
|
||||
|
||||
import { OS } from './types';
|
||||
|
||||
interface Props {
|
||||
value: OS;
|
||||
onChange(value: OS): void;
|
||||
}
|
||||
|
||||
export function OsSelector({ onChange, value }: Props) {
|
||||
return (
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<ButtonSelector
|
||||
size="small"
|
||||
value={value}
|
||||
onChange={(os: OS) => onChange(os)}
|
||||
options={[
|
||||
{
|
||||
value: 'linux',
|
||||
label: (
|
||||
<>
|
||||
<i className="fab fa-linux space-right" aria-hidden="true" />
|
||||
Linux
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'win',
|
||||
label: (
|
||||
<>
|
||||
<i
|
||||
className="fab fa-windows space-right"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Windows
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
68
app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx
Normal file
68
app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx
Normal 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>
|
||||
);
|
||||
}
|
1
app/react/edge/components/EdgeScriptForm/index.ts
Normal file
1
app/react/edge/components/EdgeScriptForm/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { EdgeScriptForm } from './EdgeScriptForm';
|
240
app/react/edge/components/EdgeScriptForm/scripts.ts
Normal file
240
app/react/edge/components/EdgeScriptForm/scripts.ts
Normal file
|
@ -0,0 +1,240 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { getAgentShortVersion } from '@/portainer/views/endpoints/helpers';
|
||||
|
||||
import { ScriptFormValues, Platform } from './types';
|
||||
|
||||
type CommandGenerator = (
|
||||
agentVersion: string,
|
||||
edgeKey: string,
|
||||
properties: ScriptFormValues,
|
||||
edgeId?: string,
|
||||
agentSecret?: string
|
||||
) => string;
|
||||
|
||||
export type CommandTab = {
|
||||
id: Platform;
|
||||
label: string;
|
||||
command: CommandGenerator;
|
||||
};
|
||||
|
||||
export const commandsTabs: Record<string, CommandTab> = {
|
||||
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(
|
||||
envVars.split(',').filter((s) => s.length > 0)
|
||||
);
|
||||
|
||||
return vars.map((s) => `-e ${s}`).join(' \\\n ');
|
||||
}
|
||||
|
||||
export function buildLinuxStandaloneCommand(
|
||||
agentVersion: string,
|
||||
edgeKey: string,
|
||||
properties: ScriptFormValues,
|
||||
edgeId?: string,
|
||||
agentSecret?: string
|
||||
) {
|
||||
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
|
||||
|
||||
const env = buildDockerEnvVars(
|
||||
envVars,
|
||||
buildDefaultEnvVars(
|
||||
edgeKey,
|
||||
allowSelfSignedCertificates,
|
||||
!edgeIdGenerator ? edgeId : undefined,
|
||||
agentSecret
|
||||
)
|
||||
);
|
||||
|
||||
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 \\
|
||||
-v /:/host \\
|
||||
-v portainer_agent_data:/data \\
|
||||
--restart always \\
|
||||
${env} \\
|
||||
--name portainer_edge_agent \\
|
||||
portainer/agent:${agentVersion}
|
||||
`;
|
||||
}
|
||||
|
||||
export function buildWindowsStandaloneCommand(
|
||||
agentVersion: string,
|
||||
edgeKey: string,
|
||||
properties: ScriptFormValues,
|
||||
edgeId?: string,
|
||||
agentSecret?: string
|
||||
) {
|
||||
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
|
||||
|
||||
const env = buildDockerEnvVars(
|
||||
envVars,
|
||||
buildDefaultEnvVars(
|
||||
edgeKey,
|
||||
allowSelfSignedCertificates,
|
||||
edgeIdGenerator ? '$Env:PORTAINER_EDGE_ID' : edgeId,
|
||||
agentSecret
|
||||
)
|
||||
);
|
||||
|
||||
return `${
|
||||
edgeIdGenerator
|
||||
? `$Env:PORTAINER_EDGE_ID = "@(${edgeIdGenerator})" \n\n`
|
||||
: ''
|
||||
}\
|
||||
docker run -d \\
|
||||
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\
|
||||
--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\
|
||||
--mount type=volume,src=portainer_agent_data,dst=C:\\data \\
|
||||
--restart always \\
|
||||
${env} \\
|
||||
--name portainer_edge_agent \\
|
||||
portainer/agent:${agentVersion}
|
||||
`;
|
||||
}
|
||||
|
||||
export function buildLinuxSwarmCommand(
|
||||
agentVersion: string,
|
||||
edgeKey: string,
|
||||
properties: ScriptFormValues,
|
||||
edgeId?: string,
|
||||
agentSecret?: string
|
||||
) {
|
||||
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
|
||||
|
||||
const env = buildDockerEnvVars(envVars, [
|
||||
...buildDefaultEnvVars(
|
||||
edgeKey,
|
||||
allowSelfSignedCertificates,
|
||||
!edgeIdGenerator ? edgeId : undefined,
|
||||
agentSecret
|
||||
),
|
||||
'AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent',
|
||||
]);
|
||||
|
||||
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 \\
|
||||
${env} \\
|
||||
--mode global \\
|
||||
--constraint 'node.platform.os == linux' \\
|
||||
--mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock \\
|
||||
--mount type=bind,src=//var/lib/docker/volumes,dst=/var/lib/docker/volumes \\
|
||||
--mount type=bind,src=//,dst=/host \\
|
||||
--mount type=volume,src=portainer_agent_data,dst=/data \\
|
||||
portainer/agent:${agentVersion}
|
||||
`;
|
||||
}
|
||||
|
||||
export function buildWindowsSwarmCommand(
|
||||
agentVersion: string,
|
||||
edgeKey: string,
|
||||
properties: ScriptFormValues,
|
||||
edgeId?: string,
|
||||
agentSecret?: string
|
||||
) {
|
||||
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
|
||||
|
||||
const env = buildDockerEnvVars(envVars, [
|
||||
...buildDefaultEnvVars(
|
||||
edgeKey,
|
||||
allowSelfSignedCertificates,
|
||||
edgeIdGenerator ? '$Env:PORTAINER_EDGE_ID' : edgeId,
|
||||
agentSecret
|
||||
),
|
||||
'AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent',
|
||||
]);
|
||||
|
||||
return `${
|
||||
edgeIdGenerator
|
||||
? `$Env: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 \\
|
||||
${env} \\
|
||||
--mode global \\
|
||||
--constraint 'node.platform.os == windows' \\
|
||||
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\
|
||||
--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\
|
||||
--mount type=volume,src=portainer_agent_data,dst=C:\\data \\
|
||||
portainer/agent:${agentVersion}
|
||||
`;
|
||||
}
|
||||
|
||||
export function buildLinuxKubernetesCommand(
|
||||
agentVersion: string,
|
||||
edgeKey: string,
|
||||
properties: ScriptFormValues,
|
||||
edgeId?: string,
|
||||
agentSecret?: string
|
||||
) {
|
||||
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
|
||||
|
||||
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(
|
||||
edgeKey: string,
|
||||
allowSelfSignedCerts: boolean,
|
||||
edgeId = '$PORTAINER_EDGE_ID',
|
||||
agentSecret = ''
|
||||
) {
|
||||
return _.compact([
|
||||
'EDGE=1',
|
||||
`EDGE_ID=${edgeId}`,
|
||||
`EDGE_KEY=${edgeKey}`,
|
||||
`EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0}`,
|
||||
agentSecret ? `AGENT_SECRET=${agentSecret}` : ``,
|
||||
]);
|
||||
}
|
20
app/react/edge/components/EdgeScriptForm/types.ts
Normal file
20
app/react/edge/components/EdgeScriptForm/types.ts
Normal 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;
|
||||
}
|
|
@ -193,6 +193,7 @@ function useAnalyticsState() {
|
|||
aciApi: 0,
|
||||
localEndpoint: 0,
|
||||
nomadEdgeAgent: 0,
|
||||
dockerEdgeAgent: 0,
|
||||
});
|
||||
|
||||
return { analytics, setAnalytics };
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -12,7 +12,7 @@ export function APITab({ onCreate }: Props) {
|
|||
<>
|
||||
<DeploymentScripts />
|
||||
|
||||
<div className="wizard-form">
|
||||
<div className="mt-5">
|
||||
<APIForm onCreate={onCreate} />
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export function AgentTab({ onCreate }: Props) {
|
|||
<>
|
||||
<DeploymentScripts />
|
||||
|
||||
<div className="wizard-form">
|
||||
<div className="mt-5">
|
||||
<AgentForm onCreate={onCreate} />
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -13,7 +13,7 @@ export function SocketTab({ onCreate }: Props) {
|
|||
<>
|
||||
<DeploymentScripts />
|
||||
|
||||
<div className="wizard-form">
|
||||
<div className="mt-5">
|
||||
<SocketForm onCreate={onCreate} />
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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 : '');
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
});
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { EdgeAgentForm } from './EdgeAgentForm';
|
|
@ -0,0 +1,9 @@
|
|||
import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create';
|
||||
|
||||
export interface FormValues {
|
||||
name: string;
|
||||
|
||||
portainerUrl: string;
|
||||
pollFrequency: number;
|
||||
meta: EnvironmentMetadata;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { EdgeAgentTab } from './EdgeAgentTab';
|
|
@ -12,7 +12,7 @@ export function MetadataFieldset() {
|
|||
const { isAdmin } = useUser();
|
||||
|
||||
return (
|
||||
<FormSection title="Metadata" isFoldable>
|
||||
<FormSection title="Metadata">
|
||||
<GroupField />
|
||||
|
||||
<TagSelector
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
export interface AnalyticsState {
|
||||
dockerAgent: number;
|
||||
dockerApi: number;
|
||||
dockerEdgeAgent: number;
|
||||
kubernetesAgent: number;
|
||||
kubernetesEdgeAgent: number;
|
||||
kaasAgent: number;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue