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

feat(podman): support add podman envs in the wizard [r8s-20] (#12056)
Some checks failed
ci / build_images (map[arch:amd64 platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
ci / build_images (map[arch:arm platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Has been cancelled
/ triage (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Test / test-client (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:linux]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
Test / test-server (map[arch:arm64 platform:linux]) (push) Has been cancelled
ci / build_manifests (push) Has been cancelled

This commit is contained in:
Ali 2024-09-25 11:55:07 +12:00 committed by GitHub
parent db616bc8a5
commit 32e94d4e4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
108 changed files with 1921 additions and 272 deletions

View file

@ -1,17 +1,25 @@
import { DockerSnapshot } from '@/react/docker/snapshots/types';
import { useIsPodman } from '@/react/portainer/environments/queries/useIsPodman';
import {
Environment,
PlatformType,
KubernetesSnapshot,
} from '@/react/portainer/environments/types';
import { getPlatformType } from '@/react/portainer/environments/utils';
import { getDockerEnvironmentType } from '@/react/portainer/environments/utils/getDockerEnvironmentType';
export function EngineVersion({ environment }: { environment: Environment }) {
const platform = getPlatformType(environment.Type);
const isPodman = useIsPodman(environment.Id);
switch (platform) {
case PlatformType.Docker:
return <DockerEngineVersion snapshot={environment.Snapshots[0]} />;
return (
<DockerEngineVersion
snapshot={environment.Snapshots[0]}
isPodman={isPodman}
/>
);
case PlatformType.Kubernetes:
return (
<KubernetesEngineVersion
@ -23,14 +31,21 @@ export function EngineVersion({ environment }: { environment: Environment }) {
}
}
function DockerEngineVersion({ snapshot }: { snapshot?: DockerSnapshot }) {
function DockerEngineVersion({
snapshot,
isPodman,
}: {
snapshot?: DockerSnapshot;
isPodman?: boolean;
}) {
if (!snapshot) {
return null;
}
const type = getDockerEnvironmentType(snapshot.Swarm, isPodman);
return (
<span className="small text-muted vertical-center">
{snapshot.Swarm ? 'Swarm' : 'Standalone'} {snapshot.DockerVersion}
{type} {snapshot.DockerVersion}
</span>
);
}

View file

@ -1,43 +1,86 @@
import { environmentTypeIcon } from '@/portainer/filters/filters';
import dockerEdge from '@/assets/images/edge_endpoint.png';
import { getEnvironmentTypeIcon } from '@/react/portainer/environments/utils';
import dockerEdge from '@/assets/ico/docker-edge-environment.svg';
import podmanEdge from '@/assets/ico/podman-edge-environment.svg';
import kube from '@/assets/images/kubernetes_endpoint.png';
import kubeEdge from '@/assets/images/kubernetes_edge_endpoint.png';
import { EnvironmentType } from '@/react/portainer/environments/types';
import kubeEdge from '@/assets/ico/kubernetes-edge-environment.svg';
import {
ContainerEngine,
EnvironmentType,
} from '@/react/portainer/environments/types';
import azure from '@/assets/ico/vendor/azure.svg';
import docker from '@/assets/ico/vendor/docker.svg';
import podman from '@/assets/ico/vendor/podman.svg';
import { Icon } from '@@/Icon';
interface Props {
type: EnvironmentType;
containerEngine?: ContainerEngine;
}
export function EnvironmentIcon({ type }: Props) {
export function EnvironmentIcon({ type, containerEngine }: Props) {
switch (type) {
case EnvironmentType.AgentOnDocker:
case EnvironmentType.Docker:
if (containerEngine === ContainerEngine.Podman) {
return (
<img
src={podman}
width="60"
alt="podman environment"
aria-hidden="true"
/>
);
}
return (
<img src={docker} width="60" alt="docker endpoint" aria-hidden="true" />
<img
src={docker}
width="60"
alt="docker environment"
aria-hidden="true"
/>
);
case EnvironmentType.Azure:
return (
<img src={azure} width="60" alt="azure endpoint" aria-hidden="true" />
<img
src={azure}
width="60"
alt="azure environment"
aria-hidden="true"
/>
);
case EnvironmentType.EdgeAgentOnDocker:
if (containerEngine === ContainerEngine.Podman) {
return (
<img
src={podmanEdge}
alt="podman edge environment"
aria-hidden="true"
/>
);
}
return (
<img src={dockerEdge} alt="docker edge endpoint" aria-hidden="true" />
<img
src={dockerEdge}
alt="docker edge environment"
aria-hidden="true"
/>
);
case EnvironmentType.KubernetesLocal:
case EnvironmentType.AgentOnKubernetes:
return <img src={kube} alt="kubernetes endpoint" aria-hidden="true" />;
return <img src={kube} alt="kubernetes environment" aria-hidden="true" />;
case EnvironmentType.EdgeAgentOnKubernetes:
return (
<img src={kubeEdge} alt="kubernetes edge endpoint" aria-hidden="true" />
<img
src={kubeEdge}
alt="kubernetes edge environment"
aria-hidden="true"
/>
);
default:
return (
<Icon
icon={environmentTypeIcon(type)}
icon={getEnvironmentTypeIcon(type, containerEngine)}
className="blue-icon !h-16 !w-16"
/>
);

View file

@ -66,7 +66,10 @@ export function EnvironmentItem({
params={dashboardRoute.params}
>
<div className="ml-2 flex justify-center self-center">
<EnvironmentIcon type={environment.Type} />
<EnvironmentIcon
type={environment.Type}
containerEngine={environment.ContainerEngine}
/>
</div>
<div className="ml-3 mr-auto flex flex-col items-start justify-center gap-3">
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">

View file

@ -265,6 +265,12 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
EnvironmentType.AgentOnDocker,
EnvironmentType.EdgeAgentOnDocker,
],
// for podman keep the env type as docker (the containerEngine distinguishes podman from docker)
[PlatformType.Podman]: [
EnvironmentType.Docker,
EnvironmentType.AgentOnDocker,
EnvironmentType.EdgeAgentOnDocker,
],
[PlatformType.Azure]: [EnvironmentType.Azure],
[PlatformType.Kubernetes]: [
EnvironmentType.KubernetesLocal,

View file

@ -171,6 +171,13 @@ function getConnectionTypeOptions(platformTypes: PlatformType[]) {
ConnectionType.EdgeAgentStandard,
ConnectionType.EdgeAgentAsync,
],
[PlatformType.Podman]: [
// api includes a socket connection, so keep this for podman
ConnectionType.API,
ConnectionType.Agent,
ConnectionType.EdgeAgentStandard,
ConnectionType.EdgeAgentAsync,
],
[PlatformType.Azure]: [ConnectionType.API],
[PlatformType.Kubernetes]: [
ConnectionType.Agent,

View file

@ -5,7 +5,7 @@ import { Select } from '@@/form-components/Input';
const typeOptions = [
{ label: 'Swarm', value: StackType.DockerSwarm },
{ label: 'Standalone', value: StackType.DockerCompose },
{ label: 'Standalone / Podman', value: StackType.DockerCompose },
];
export function TemplateTypeSelector({

View file

@ -1,28 +1,41 @@
import { CellContext } from '@tanstack/react-table';
import { environmentTypeIcon } from '@/portainer/filters/filters';
import {
Environment,
EnvironmentType,
} from '@/react/portainer/environments/types';
import { getPlatformTypeName } from '@/react/portainer/environments/utils';
getEnvironmentTypeIcon,
getPlatformTypeName,
} from '@/react/portainer/environments/utils';
import { Icon } from '@@/Icon';
import { EnvironmentListItem } from '../types';
import { EnvironmentType, ContainerEngine } from '../../types';
import { columnHelper } from './helper';
export const type = columnHelper.accessor('Type', {
header: 'Type',
cell: Cell,
});
type TypeCellContext = {
type: EnvironmentType;
containerEngine?: ContainerEngine;
};
function Cell({ getValue }: CellContext<Environment, EnvironmentType>) {
const type = getValue();
export const type = columnHelper.accessor(
(rowItem): TypeCellContext => ({
type: rowItem.Type,
containerEngine: rowItem.ContainerEngine,
}),
{
header: 'Type',
cell: Cell,
id: 'Type',
}
);
function Cell({ getValue }: CellContext<EnvironmentListItem, TypeCellContext>) {
const { type, containerEngine } = getValue();
return (
<span className="flex items-center gap-1">
<Icon icon={environmentTypeIcon(type)} />
{getPlatformTypeName(type)}
<Icon icon={getEnvironmentTypeIcon(type, containerEngine)} />
{getPlatformTypeName(type, containerEngine)}
</span>
);
}

View file

@ -7,7 +7,11 @@ import { type EnvironmentGroupId } from '@/react/portainer/environments/environm
import { type TagId } from '@/portainer/tags/types';
import { EdgeAsyncIntervalsValues } from '@/react/edge/components/EdgeAsyncIntervalsForm';
import { type Environment, EnvironmentCreationTypes } from '../types';
import {
type Environment,
ContainerEngine,
EnvironmentCreationTypes,
} from '../types';
import { buildUrl } from './utils';
@ -21,6 +25,7 @@ interface CreateLocalDockerEnvironment {
socketPath?: string;
publicUrl?: string;
meta?: EnvironmentMetadata;
containerEngine?: ContainerEngine;
}
export async function createLocalDockerEnvironment({
@ -28,6 +33,7 @@ export async function createLocalDockerEnvironment({
socketPath = '',
publicUrl = '',
meta = { tagIds: [] },
containerEngine,
}: CreateLocalDockerEnvironment) {
const url = prefixPath(socketPath);
@ -38,6 +44,7 @@ export async function createLocalDockerEnvironment({
url,
publicUrl,
meta,
containerEngine,
}
);
@ -115,6 +122,7 @@ export interface EnvironmentOptions {
pollFrequency?: number;
edge?: EdgeSettings;
tunnelServerAddr?: string;
containerEngine?: ContainerEngine;
}
interface CreateRemoteEnvironment {
@ -125,6 +133,7 @@ interface CreateRemoteEnvironment {
>;
url: string;
options?: Omit<EnvironmentOptions, 'url'>;
containerEngine?: ContainerEngine;
}
export async function createRemoteEnvironment({
@ -143,11 +152,13 @@ export interface CreateAgentEnvironmentValues {
name: string;
environmentUrl: string;
meta: EnvironmentMetadata;
containerEngine?: ContainerEngine;
}
export function createAgentEnvironment({
name,
environmentUrl,
containerEngine = ContainerEngine.Docker,
meta = { tagIds: [] },
}: CreateAgentEnvironmentValues) {
return createRemoteEnvironment({
@ -160,6 +171,7 @@ export function createAgentEnvironment({
skipVerify: true,
skipClientVerify: true,
},
containerEngine,
},
});
}
@ -171,6 +183,7 @@ interface CreateEdgeAgentEnvironment {
meta?: EnvironmentMetadata;
pollFrequency: number;
edge: EdgeSettings;
containerEngine: ContainerEngine;
}
export function createEdgeAgentEnvironment({
@ -179,6 +192,7 @@ export function createEdgeAgentEnvironment({
meta = { tagIds: [] },
pollFrequency,
edge,
containerEngine,
}: CreateEdgeAgentEnvironment) {
return createEnvironment(
name,
@ -192,6 +206,7 @@ export function createEdgeAgentEnvironment({
pollFrequency,
edge,
meta,
containerEngine,
}
);
}
@ -207,7 +222,8 @@ async function createEnvironment(
};
if (options) {
const { groupId, tagIds = [] } = options.meta || {};
const { tls, azure, meta, containerEngine } = options;
const { groupId, tagIds = [] } = meta || {};
payload = {
...payload,
@ -216,10 +232,9 @@ async function createEnvironment(
GroupID: groupId,
TagIds: arrayToJson(tagIds),
EdgeCheckinInterval: options.pollFrequency,
ContainerEngine: containerEngine,
};
const { tls, azure } = options;
if (tls) {
payload = {
...payload,

View file

@ -0,0 +1,15 @@
import { ContainerEngine, EnvironmentId } from '../types';
import { useEnvironment } from './useEnvironment';
/**
* useIsPodman returns true if the current environment is using podman as container engine.
* @returns isPodman boolean, can also be undefined if the environment hasn't loaded yet.
*/
export function useIsPodman(envId: EnvironmentId) {
const { data: isPodman } = useEnvironment(
envId,
(env) => env.ContainerEngine === ContainerEngine.Podman
);
return isPodman;
}

View file

@ -4,6 +4,9 @@ import { DockerSnapshot } from '@/react/docker/snapshots/types';
export type EnvironmentId = number;
/**
* matches portainer.EndpointType in app/portainer.go
*/
export enum EnvironmentType {
// Docker represents an environment(endpoint) connected to a Docker environment(endpoint)
Docker = 1,
@ -124,6 +127,7 @@ export type Environment = {
Agent: { Version: string };
Id: EnvironmentId;
Type: EnvironmentType;
ContainerEngine?: ContainerEngine;
TagIds: TagId[];
GroupId: EnvironmentGroupId;
DeploymentOptions: DeploymentOptions | null;
@ -168,8 +172,14 @@ export enum EnvironmentCreationTypes {
KubeConfigEnvironment,
}
export enum ContainerEngine {
Docker = 'docker',
Podman = 'podman',
}
export enum PlatformType {
Docker,
Kubernetes,
Azure,
Podman,
}

View file

@ -1,8 +1,10 @@
import { getPlatformType } from '@/react/portainer/environments/utils';
import {
ContainerEngine,
EnvironmentType,
PlatformType,
} from '@/react/portainer/environments/types';
import Podman from '@/assets/ico/vendor/podman.svg?c';
import Docker from './docker.svg?c';
import Azure from './azure.svg?c';
@ -12,12 +14,16 @@ const icons: {
[key in PlatformType]: SvgrComponent;
} = {
[PlatformType.Docker]: Docker,
[PlatformType.Podman]: Podman,
[PlatformType.Kubernetes]: Kubernetes,
[PlatformType.Azure]: Azure,
};
export function getPlatformIcon(type: EnvironmentType) {
const platform = getPlatformType(type);
export function getPlatformIcon(
type: EnvironmentType,
containerEngine?: ContainerEngine
) {
const platform = getPlatformType(type, containerEngine);
return icons[platform];
}

View file

@ -0,0 +1,6 @@
export function getDockerEnvironmentType(isSwarm: boolean, isPodman?: boolean) {
if (isPodman) {
return 'Podman';
}
return isSwarm ? 'Swarm' : 'Standalone';
}

View file

@ -1,6 +1,21 @@
import { Environment, EnvironmentType, PlatformType } from '../types';
import { Cloud } from 'lucide-react';
export function getPlatformType(envType: EnvironmentType) {
import Kube from '@/assets/ico/kube.svg?c';
import PodmanIcon from '@/assets/ico/vendor/podman-icon.svg?c';
import DockerIcon from '@/assets/ico/vendor/docker-icon.svg?c';
import MicrosoftIcon from '@/assets/ico/vendor/microsoft-icon.svg?c';
import {
Environment,
EnvironmentType,
ContainerEngine,
PlatformType,
} from '../types';
export function getPlatformType(
envType: EnvironmentType,
containerEngine?: ContainerEngine
) {
switch (envType) {
case EnvironmentType.KubernetesLocal:
case EnvironmentType.AgentOnKubernetes:
@ -9,6 +24,9 @@ export function getPlatformType(envType: EnvironmentType) {
case EnvironmentType.Docker:
case EnvironmentType.AgentOnDocker:
case EnvironmentType.EdgeAgentOnDocker:
if (containerEngine === ContainerEngine.Podman) {
return PlatformType.Podman;
}
return PlatformType.Docker;
case EnvironmentType.Azure:
return PlatformType.Azure;
@ -25,8 +43,11 @@ export function isKubernetesEnvironment(envType: EnvironmentType) {
return getPlatformType(envType) === PlatformType.Kubernetes;
}
export function getPlatformTypeName(envType: EnvironmentType): string {
return PlatformType[getPlatformType(envType)];
export function getPlatformTypeName(
envType: EnvironmentType,
containerEngine?: ContainerEngine
): string {
return PlatformType[getPlatformType(envType, containerEngine)];
}
export function isAgentEnvironment(envType: EnvironmentType) {
@ -104,3 +125,27 @@ export function getDashboardRoute(environment: Environment) {
}
}
}
export function getEnvironmentTypeIcon(
type: EnvironmentType,
containerEngine?: ContainerEngine
) {
switch (type) {
case EnvironmentType.Azure:
return MicrosoftIcon;
case EnvironmentType.EdgeAgentOnDocker:
return Cloud;
case EnvironmentType.AgentOnKubernetes:
case EnvironmentType.EdgeAgentOnKubernetes:
case EnvironmentType.KubernetesLocal:
return Kube;
case EnvironmentType.AgentOnDocker:
case EnvironmentType.Docker:
if (containerEngine === ContainerEngine.Podman) {
return PodmanIcon;
}
return DockerIcon;
default:
throw new Error(`type ${type}-${EnvironmentType[type]} is not supported`);
}
}

View file

@ -15,6 +15,7 @@ import {
EnvironmentOptionValue,
existingEnvironmentTypes,
newEnvironmentTypes,
environmentTypes,
} from './environment-types';
export function EnvironmentTypeSelectView() {
@ -65,6 +66,7 @@ export function EnvironmentTypeSelectView() {
disabled={types.length === 0}
data-cy="start-wizard-button"
onClick={() => startWizard()}
className="!ml-0"
>
Start Wizard
</Button>
@ -80,11 +82,6 @@ export function EnvironmentTypeSelectView() {
return;
}
const environmentTypes = [
...existingEnvironmentTypes,
...newEnvironmentTypes,
];
const steps = _.compact(
types.map((id) => environmentTypes.find((eType) => eType.id === id))
);

View file

@ -1,5 +1,6 @@
import { FeatureId } from '@/react/portainer/feature-flags/enums';
import Docker from '@/assets/ico/vendor/docker.svg?c';
import Podman from '@/assets/ico/vendor/podman.svg?c';
import Kubernetes from '@/assets/ico/vendor/kubernetes.svg?c';
import Azure from '@/assets/ico/vendor/azure.svg?c';
import KaaS from '@/assets/ico/vendor/kaas-icon.svg?c';
@ -10,6 +11,7 @@ import { BoxSelectorOption } from '@@/BoxSelector';
export type EnvironmentOptionValue =
| 'dockerStandalone'
| 'dockerSwarm'
| 'podman'
| 'kubernetes'
| 'aci'
| 'kaas'
@ -20,7 +22,6 @@ export interface EnvironmentOption
id: EnvironmentOptionValue;
value: EnvironmentOptionValue;
}
export const existingEnvironmentTypes: EnvironmentOption[] = [
{
id: 'dockerStandalone',
@ -38,6 +39,14 @@ export const existingEnvironmentTypes: EnvironmentOption[] = [
iconType: 'logo',
description: 'Connect to Docker Swarm via URL/IP, API or Socket',
},
{
id: 'podman',
value: 'podman',
label: 'Podman',
icon: Podman,
iconType: 'logo',
description: 'Connect to Podman via URL/IP or Socket',
},
{
id: 'kubernetes',
value: 'kubernetes',
@ -80,7 +89,7 @@ export const newEnvironmentTypes: EnvironmentOption[] = [
},
];
export const environmentTypes = [
export const environmentTypes: EnvironmentOption[] = [
...existingEnvironmentTypes,
...newEnvironmentTypes,
];
@ -88,6 +97,7 @@ export const environmentTypes = [
export const formTitles: Record<EnvironmentOptionValue, string> = {
dockerStandalone: 'Connect to your Docker Standalone environment',
dockerSwarm: 'Connect to your Docker Swarm environment',
podman: 'Connect to your Podman environment',
kubernetes: 'Connect to your Kubernetes environment',
aci: 'Connect to your ACI environment',
kaas: 'Provision a KaaS environment',

View file

@ -1 +1 @@
export { EnvironmentTypeSelectView } from './EndpointTypeView';
export { EnvironmentTypeSelectView } from './EnvironmentTypeSelectView';

View file

@ -7,7 +7,7 @@
.wizard-wrapper {
display: grid;
grid-template-columns: 1fr 400px;
grid-template-columns: 2fr minmax(300px, 1fr);
grid-template-areas:
'main sidebar'
'footer sidebar';

View file

@ -22,6 +22,7 @@ import {
EnvironmentOptionValue,
environmentTypes,
formTitles,
EnvironmentOption,
} from '../EnvironmentTypeSelectView/environment-types';
import { WizardDocker } from './WizardDocker';
@ -30,6 +31,7 @@ import { WizardKubernetes } from './WizardKubernetes';
import { AnalyticsState, AnalyticsStateKey } from './types';
import styles from './EnvironmentsCreationView.module.css';
import { WizardEndpointsList } from './WizardEndpointsList';
import { WizardPodman } from './WizardPodman';
export function EnvironmentCreationView() {
const {
@ -161,7 +163,7 @@ function useParamEnvironmentTypes(): EnvironmentOptionValue[] {
}
function useStepper(
steps: (typeof environmentTypes)[number][],
steps: EnvironmentOption[][number][],
onFinish: () => void
) {
const [currentStepIndex, setCurrentStepIndex] = useState(0);
@ -197,6 +199,8 @@ function useStepper(
case 'dockerStandalone':
case 'dockerSwarm':
return WizardDocker;
case 'podman':
return WizardPodman;
case 'aci':
return WizardAzure;
case 'kubernetes':
@ -211,14 +215,18 @@ function useAnalyticsState() {
const [analytics, setAnalyticsState] = useState<AnalyticsState>({
dockerAgent: 0,
dockerApi: 0,
dockerEdgeAgentAsync: 0,
dockerEdgeAgentStandard: 0,
podmanAgent: 0,
podmanEdgeAgentAsync: 0,
podmanEdgeAgentStandard: 0,
podmanLocalEnvironment: 0,
kubernetesAgent: 0,
kubernetesEdgeAgentAsync: 0,
kubernetesEdgeAgentStandard: 0,
kaasAgent: 0,
aciApi: 0,
localEndpoint: 0,
dockerEdgeAgentAsync: 0,
dockerEdgeAgentStandard: 0,
});
return { analytics, setAnalytics };

View file

@ -4,6 +4,7 @@ import { CopyButton } from '@@/buttons/CopyButton';
import { Code } from '@@/Code';
import { NavTabs } from '@@/NavTabs';
import { NavContainer } from '@@/NavTabs/NavContainer';
import { TextTip } from '@@/Tip/TextTip';
const deployments = [
{
@ -45,10 +46,10 @@ interface DeployCodeProps {
function DeployCode({ code }: DeployCodeProps) {
return (
<>
<span className="text-muted small">
<TextTip color="blue" className="mb-1">
When using the socket, ensure that you have started the Portainer
container with the following Docker flag:
</span>
</TextTip>
<Code>{code}</Code>
<div className="mt-2">

View file

@ -1,4 +1,7 @@
import { Environment } from '@/react/portainer/environments/types';
import {
ContainerEngine,
Environment,
} from '@/react/portainer/environments/types';
import { AgentForm } from '../../shared/AgentForm/AgentForm';
@ -15,7 +18,10 @@ export function AgentTab({ onCreate, isDockerStandalone }: Props) {
<DeploymentScripts isDockerStandalone={isDockerStandalone} />
<div className="mt-5">
<AgentForm onCreate={onCreate} />
<AgentForm
onCreate={onCreate}
containerEngine={ContainerEngine.Docker}
/>
</div>
</>
);

View file

@ -4,7 +4,10 @@ import { Plug2 } from 'lucide-react';
import { useCreateLocalDockerEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation';
import { notifySuccess } from '@/portainer/services/notifications';
import { Environment } from '@/react/portainer/environments/types';
import {
ContainerEngine,
Environment,
} from '@/react/portainer/environments/types';
import { LoadingButton } from '@@/buttons/LoadingButton';
import { FormControl } from '@@/form-components/FormControl';
@ -19,9 +22,10 @@ import { FormValues } from './types';
interface Props {
onCreate(environment: Environment): void;
containerEngine: ContainerEngine;
}
export function SocketForm({ onCreate }: Props) {
export function SocketForm({ onCreate, containerEngine }: Props) {
const [formKey, clearForm] = useReducer((state) => state + 1, 0);
const initialValues: FormValues = {
name: '',
@ -74,6 +78,7 @@ export function SocketForm({ onCreate }: Props) {
name: values.name,
socketPath: values.overridePath ? values.socketPath : '',
meta: values.meta,
containerEngine,
},
{
onSuccess(environment) {

View file

@ -1,4 +1,7 @@
import { Environment } from '@/react/portainer/environments/types';
import {
ContainerEngine,
Environment,
} from '@/react/portainer/environments/types';
import { DeploymentScripts } from '../APITab/DeploymentScripts';
@ -14,7 +17,10 @@ export function SocketTab({ onCreate }: Props) {
<DeploymentScripts />
<div className="mt-5">
<SocketForm onCreate={onCreate} />
<SocketForm
onCreate={onCreate}
containerEngine={ContainerEngine.Docker}
/>
</div>
</>
);

View file

@ -2,7 +2,10 @@ import { useState } from 'react';
import { Zap, Network, Plug2 } from 'lucide-react';
import _ from 'lodash';
import { Environment } from '@/react/portainer/environments/types';
import {
ContainerEngine,
Environment,
} from '@/react/portainer/environments/types';
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import EdgeAgentStandardIcon from '@/react/edge/components/edge-agent-standard.svg?c';
@ -64,6 +67,8 @@ const options: BoxSelectorOption<
},
]);
const containerEngine = ContainerEngine.Docker;
export function WizardDocker({ onCreate, isDockerStandalone }: Props) {
const [creationType, setCreationType] = useState(options[0].value);
@ -135,6 +140,7 @@ export function WizardDocker({ onCreate, isDockerStandalone }: Props) {
? [commandsTabs.standaloneWindow]
: [commandsTabs.swarmWindows],
}}
containerEngine={containerEngine}
/>
);
case 'edgeAgentAsync':
@ -152,6 +158,7 @@ export function WizardDocker({ onCreate, isDockerStandalone }: Props) {
? [commandsTabs.standaloneWindow]
: [commandsTabs.swarmWindows],
}}
containerEngine={containerEngine}
/>
);
default:

View file

@ -22,8 +22,6 @@
.wizard-list-image {
grid-area: image;
font-size: 35px;
color: #337ab7;
}
.wizard-list-title {

View file

@ -1,15 +1,13 @@
import { Plug2 } from 'lucide-react';
import clsx from 'clsx';
import { endpointTypeName, stripProtocol } from '@/portainer/filters/filters';
import {
environmentTypeIcon,
endpointTypeName,
stripProtocol,
} from '@/portainer/filters/filters';
import { EnvironmentId } from '@/react/portainer/environments/types';
import {
getEnvironmentTypeIcon,
isEdgeEnvironment,
isUnassociatedEdgeEnvironment,
} from '@/react/portainer/environments/utils';
import { EnvironmentId } from '@/react/portainer/environments/types';
import {
ENVIRONMENTS_POLLING_INTERVAL,
useEnvironmentList,
@ -51,9 +49,17 @@ export function WizardEndpointsList({ environmentIds }: Props) {
<WidgetBody>
{environments.map((environment) => (
<div className={styles.wizardListWrapper} key={environment.Id}>
<div className={styles.wizardListImage}>
<div
className={clsx(
styles.wizardListImage,
'text-blue-8 th-dark:text-blue-7 th-highcontrast:text-white text-5xl'
)}
>
<Icon
icon={environmentTypeIcon(environment.Type)}
icon={getEnvironmentTypeIcon(
environment.Type,
environment.ContainerEngine
)}
className="mr-1"
/>
</div>

View file

@ -0,0 +1,27 @@
import {
ContainerEngine,
Environment,
} from '@/react/portainer/environments/types';
import { AgentForm } from '../../shared/AgentForm/AgentForm';
import { DeploymentScripts } from './DeploymentScripts';
interface Props {
onCreate(environment: Environment): void;
}
export function AgentTab({ onCreate }: Props) {
return (
<>
<DeploymentScripts />
<div className="mt-5">
<AgentForm
onCreate={onCreate}
containerEngine={ContainerEngine.Podman}
/>
</div>
</>
);
}

View file

@ -0,0 +1,86 @@
import { useState } from 'react';
import { useAgentDetails } from '@/react/portainer/environments/queries/useAgentDetails';
import { CopyButton } from '@@/buttons/CopyButton';
import { Code } from '@@/Code';
import { NavTabs } from '@@/NavTabs';
import { NavContainer } from '@@/NavTabs/NavContainer';
const deploymentPodman = [
{
id: 'all',
label: 'Linux (CentOS)',
command: linuxPodmanCommandRootful,
},
];
export function DeploymentScripts() {
const deployments = deploymentPodman;
const [deployType, setDeployType] = useState(deployments[0].id);
const agentDetailsQuery = useAgentDetails();
if (!agentDetailsQuery) {
return null;
}
const { agentVersion, agentSecret } = agentDetailsQuery;
const options = deployments.map((c) => {
const code = c.command(agentVersion, agentSecret);
return {
id: c.id,
label: c.label,
children: <DeployCode code={code} />,
};
});
return (
<NavContainer>
<NavTabs
options={options}
onSelect={(id: string) => setDeployType(id)}
selectedId={deployType}
/>
</NavContainer>
);
}
interface DeployCodeProps {
code: string;
}
function DeployCode({ code }: DeployCodeProps) {
return (
<>
<div className="code-script">
<Code>{code}</Code>
</div>
<div className="mt-2">
<CopyButton copyText={code} data-cy="copy-deployment-script">
Copy command
</CopyButton>
</div>
</>
);
}
function linuxPodmanCommandRootful(agentVersion: string, agentSecret: string) {
const secret =
agentSecret === '' ? '' : `\\\n -e AGENT_SECRET=${agentSecret} `;
return `sudo systemctl enable --now podman.socket\n
sudo podman volume create portainer\n
sudo podman run -d \\
-p 9001:9001 ${secret}\\
--name portainer_agent \\
--restart=always \\
--privileged \\
-v /run/podman/podman.sock:/var/run/docker.sock \\
-v /var/lib/containers/storage/volumes:/var/lib/docker/volumes \\
-v /:/host \\
portainer/agent:${agentVersion}
`;
}

View file

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

View file

@ -0,0 +1,68 @@
import { useState } from 'react';
import { CopyButton } from '@@/buttons/CopyButton';
import { Code } from '@@/Code';
import { NavTabs } from '@@/NavTabs';
import { NavContainer } from '@@/NavTabs/NavContainer';
import { TextTip } from '@@/Tip/TextTip';
const deployments = [
{
id: 'linux',
label: 'Linux (CentOS)',
command: `sudo systemctl enable --now podman.socket`,
},
];
export function DeploymentScripts() {
const [deployType, setDeployType] = useState(deployments[0].id);
const options = deployments.map((c) => ({
id: c.id,
label: c.label,
children: <DeployCode code={c.command} />,
}));
return (
<NavContainer>
<NavTabs
options={options}
onSelect={(id: string) => setDeployType(id)}
selectedId={deployType}
/>
</NavContainer>
);
}
interface DeployCodeProps {
code: string;
}
function DeployCode({ code }: DeployCodeProps) {
const bindMountCode = `-v "/run/podman/podman.sock:/var/run/docker.sock"`;
return (
<>
<TextTip color="blue" className="mb-1">
When using the socket, ensure that you have started the Portainer
container with the following Podman flag:
</TextTip>
<Code>{bindMountCode}</Code>
<div className="mt-2 mb-4">
<CopyButton copyText={bindMountCode} data-cy="copy-deployment-command">
Copy command
</CopyButton>
</div>
<TextTip color="blue" className="mb-1">
To use the socket, ensure that you have started the Podman rootful
socket:
</TextTip>
<Code>{code}</Code>
<div className="mt-2">
<CopyButton copyText={code} data-cy="copy-deployment-command">
Copy command
</CopyButton>
</div>
</>
);
}

View file

@ -0,0 +1,125 @@
import { Field, Form, Formik, useFormikContext } from 'formik';
import { useReducer } from 'react';
import { Plug2 } from 'lucide-react';
import { notifySuccess } from '@/portainer/services/notifications';
import { useCreateLocalDockerEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation';
import {
ContainerEngine,
Environment,
} from '@/react/portainer/environments/types';
import { LoadingButton } from '@@/buttons/LoadingButton';
import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
import { SwitchField } from '@@/form-components/SwitchField';
import { NameField } from '../../shared/NameField';
import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
import { useValidation } from './SocketForm.validation';
import { FormValues } from './types';
interface Props {
onCreate(environment: Environment): void;
containerEngine: ContainerEngine;
}
export function SocketForm({ onCreate, containerEngine }: Props) {
const [formKey, clearForm] = useReducer((state) => state + 1, 0);
const initialValues: FormValues = {
name: '',
socketPath: '',
overridePath: false,
meta: { groupId: 1, tagIds: [] },
};
const mutation = useCreateLocalDockerEnvironmentMutation();
const validation = useValidation();
return (
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validationSchema={validation}
validateOnMount
key={formKey}
>
{({ isValid, dirty }) => (
<Form>
<NameField />
<OverrideSocketFieldset />
<MoreSettingsSection />
<div className="form-group">
<div className="col-sm-12">
<LoadingButton
className="wizard-connect-button vertical-center"
data-cy="docker-socket-connect-button"
loadingText="Connecting environment..."
isLoading={mutation.isLoading}
disabled={!dirty || !isValid}
icon={Plug2}
>
Connect
</LoadingButton>
</div>
</div>
</Form>
)}
</Formik>
);
function handleSubmit(values: FormValues) {
mutation.mutate(
{
name: values.name,
socketPath: values.overridePath ? values.socketPath : '',
meta: values.meta,
containerEngine,
},
{
onSuccess(environment) {
notifySuccess('Environment created', environment.Name);
clearForm();
onCreate(environment);
},
}
);
}
}
function OverrideSocketFieldset() {
const { values, setFieldValue, errors } = useFormikContext<FormValues>();
return (
<>
<div className="form-group">
<div className="col-sm-12">
<SwitchField
checked={values.overridePath}
data-cy="create-docker-env-socket-override-switch"
onChange={(checked) => setFieldValue('overridePath', checked)}
label="Override default socket path"
labelClass="col-sm-3 col-lg-2"
/>
</div>
</div>
{values.overridePath && (
<FormControl
label="Socket Path"
tooltip="Path to the Podman socket. Remember to bind-mount the socket, see the important notice above for more information."
errors={errors.socketPath}
>
<Field
name="socketPath"
as={Input}
placeholder="e.g. /run/podman/podman.sock (on Linux)"
/>
</FormControl>
)}
</>
);
}

View file

@ -0,0 +1,23 @@
import { boolean, object, SchemaOf, string } from 'yup';
import { metadataValidation } from '../../shared/MetadataFieldset/validation';
import { useNameValidation } from '../../shared/NameField';
import { FormValues } from './types';
export function useValidation(): SchemaOf<FormValues> {
return object({
name: useNameValidation(),
meta: metadataValidation(),
overridePath: boolean().default(false),
socketPath: string()
.default('')
.when('overridePath', (overridePath, schema) =>
overridePath
? schema.required(
'Socket Path is required when override path is enabled'
)
: schema
),
});
}

View file

@ -0,0 +1,33 @@
import {
ContainerEngine,
Environment,
} from '@/react/portainer/environments/types';
import { TextTip } from '@@/Tip/TextTip';
import { DeploymentScripts } from './DeploymentScripts';
import { SocketForm } from './SocketForm';
interface Props {
onCreate(environment: Environment): void;
}
export function SocketTab({ onCreate }: Props) {
return (
<>
<TextTip color="orange" className="mb-2" inline={false}>
To connect via socket, Portainer server must be running in a Podman
container.
</TextTip>
<DeploymentScripts />
<div className="mt-5">
<SocketForm
onCreate={onCreate}
containerEngine={ContainerEngine.Podman}
/>
</div>
</>
);
}

View file

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

View file

@ -0,0 +1,8 @@
import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create';
export interface FormValues {
name: string;
socketPath: string;
overridePath: boolean;
meta: EnvironmentMetadata;
}

View file

@ -0,0 +1,133 @@
import { useState } from 'react';
import { Zap, Plug2 } from 'lucide-react';
import _ from 'lodash';
import {
ContainerEngine,
Environment,
} from '@/react/portainer/environments/types';
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import EdgeAgentStandardIcon from '@/react/edge/components/edge-agent-standard.svg?c';
import EdgeAgentAsyncIcon from '@/react/edge/components/edge-agent-async.svg?c';
import { BoxSelector, type BoxSelectorOption } from '@@/BoxSelector';
import { BadgeIcon } from '@@/BadgeIcon';
import { TextTip } from '@@/Tip/TextTip';
import { AnalyticsStateKey } from '../types';
import { EdgeAgentTab } from '../shared/EdgeAgentTab';
import { AgentTab } from './AgentTab';
import { SocketTab } from './SocketTab';
interface Props {
onCreate(environment: Environment, analytics: AnalyticsStateKey): void;
}
const options: BoxSelectorOption<
'agent' | 'api' | 'socket' | 'edgeAgentStandard' | 'edgeAgentAsync'
>[] = _.compact([
{
id: 'agent',
icon: <BadgeIcon icon={Zap} size="3xl" />,
label: 'Agent',
description: '',
value: 'agent',
},
{
id: 'socket',
icon: <BadgeIcon icon={Plug2} size="3xl" />,
label: 'Socket',
description: '',
value: 'socket',
},
{
id: 'edgeAgentStandard',
icon: <BadgeIcon icon={EdgeAgentStandardIcon} size="3xl" />,
label: 'Edge Agent Standard',
description: '',
value: 'edgeAgentStandard',
},
isBE && {
id: 'edgeAgentAsync',
icon: <BadgeIcon icon={EdgeAgentAsyncIcon} size="3xl" />,
label: 'Edge Agent Async',
description: '',
value: 'edgeAgentAsync',
},
]);
const containerEngine = ContainerEngine.Podman;
export function WizardPodman({ onCreate }: Props) {
const [creationType, setCreationType] = useState(options[0].value);
const tab = getTab(creationType);
return (
<div className="form-horizontal">
<BoxSelector
onChange={(v) => setCreationType(v)}
options={options}
value={creationType}
radioName="creation-type"
/>
<TextTip color="orange" className="mb-2" inline={false}>
Currently, Portainer only supports <b>Podman 5</b> running in rootful
(privileged) mode on <b>CentOS 9</b> Linux environments. Rootless mode
and other Linux distros may work, but aren&apos;t officially supported.
</TextTip>
{tab}
</div>
);
function getTab(
creationType:
| 'agent'
| 'api'
| 'socket'
| 'edgeAgentStandard'
| 'edgeAgentAsync'
) {
switch (creationType) {
case 'agent':
return (
<AgentTab
onCreate={(environment) => onCreate(environment, 'podmanAgent')}
/>
);
case 'socket':
return (
<SocketTab
onCreate={(environment) =>
onCreate(environment, 'podmanLocalEnvironment')
}
/>
);
case 'edgeAgentStandard':
return (
<EdgeAgentTab
onCreate={(environment) =>
onCreate(environment, 'podmanEdgeAgentStandard')
}
commands={[commandsTabs.podmanLinux]}
containerEngine={containerEngine}
/>
);
case 'edgeAgentAsync':
return (
<EdgeAgentTab
asyncMode
onCreate={(environment) =>
onCreate(environment, 'podmanEdgeAgentAsync')
}
commands={[commandsTabs.podmanLinux]}
containerEngine={containerEngine}
/>
);
default:
return null;
}
}
}

View file

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

View file

@ -4,7 +4,10 @@ import { Plug2 } from 'lucide-react';
import { useCreateAgentEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation';
import { notifySuccess } from '@/portainer/services/notifications';
import { Environment } from '@/react/portainer/environments/types';
import {
ContainerEngine,
Environment,
} from '@/react/portainer/environments/types';
import { CreateAgentEnvironmentValues } from '@/react/portainer/environments/environment.service/create';
import { LoadingButton } from '@@/buttons/LoadingButton';
@ -18,6 +21,7 @@ import { useValidation } from './AgentForm.validation';
interface Props {
onCreate(environment: Environment): void;
envDefaultPort?: string;
containerEngine?: ContainerEngine;
}
const initialValues: CreateAgentEnvironmentValues = {
@ -29,7 +33,11 @@ const initialValues: CreateAgentEnvironmentValues = {
},
};
export function AgentForm({ onCreate, envDefaultPort }: Props) {
export function AgentForm({
onCreate,
envDefaultPort,
containerEngine = ContainerEngine.Docker,
}: Props) {
const [formKey, clearForm] = useReducer((state) => state + 1, 0);
const mutation = useCreateAgentEnvironmentMutation();
@ -70,12 +78,15 @@ export function AgentForm({ onCreate, envDefaultPort }: Props) {
);
function handleSubmit(values: CreateAgentEnvironmentValues) {
mutation.mutate(values, {
onSuccess(environment) {
notifySuccess('Environment created', environment.Name);
clearForm();
onCreate(environment);
},
});
mutation.mutate(
{ ...values, containerEngine },
{
onSuccess(environment) {
notifySuccess('Environment created', environment.Name);
clearForm();
onCreate(environment);
},
}
);
}
}

View file

@ -1,4 +1,4 @@
import { object, SchemaOf, string } from 'yup';
import { mixed, object, SchemaOf, string } from 'yup';
import { CreateAgentEnvironmentValues } from '@/react/portainer/environments/environment.service/create';
@ -10,6 +10,7 @@ export function useValidation(): SchemaOf<CreateAgentEnvironmentValues> {
name: useNameValidation(),
environmentUrl: environmentValidation(),
meta: metadataValidation(),
containerEngine: mixed().oneOf(['docker', 'podman']),
});
}

View file

@ -1,7 +1,10 @@
import { Formik, Form } from 'formik';
import { Plug2 } from 'lucide-react';
import { Environment } from '@/react/portainer/environments/types';
import {
ContainerEngine,
Environment,
} from '@/react/portainer/environments/types';
import { useCreateEdgeAgentEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation';
import { Settings } from '@/react/portainer/settings/types';
import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField';
@ -26,9 +29,15 @@ interface Props {
onCreate(environment: Environment): void;
readonly: boolean;
asyncMode: boolean;
containerEngine: ContainerEngine;
}
export function EdgeAgentForm({ onCreate, readonly, asyncMode }: Props) {
export function EdgeAgentForm({
onCreate,
readonly,
asyncMode,
containerEngine,
}: Props) {
const settingsQuery = useSettings();
const createMutation = useCreateEdgeAgentEnvironmentMutation();
@ -100,6 +109,7 @@ export function EdgeAgentForm({ onCreate, readonly, asyncMode }: Props) {
...values.edge,
asyncMode,
},
containerEngine,
},
{
onSuccess(environment) {

View file

@ -1,7 +1,10 @@
import { v4 as uuid } from 'uuid';
import { useReducer, useState } from 'react';
import { Environment } from '@/react/portainer/environments/types';
import {
ContainerEngine,
Environment,
} from '@/react/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';
@ -15,9 +18,15 @@ interface Props {
onCreate: (environment: Environment) => void;
commands: CommandTab[] | Partial<Record<OS, CommandTab[]>>;
asyncMode?: boolean;
containerEngine?: ContainerEngine;
}
export function EdgeAgentTab({ onCreate, commands, asyncMode = false }: Props) {
export function EdgeAgentTab({
onCreate,
commands,
asyncMode = false,
containerEngine = ContainerEngine.Docker,
}: Props) {
const [edgeInfo, setEdgeInfo] = useState<EdgeInfo>();
const [formKey, clearForm] = useReducer((state) => state + 1, 0);
@ -28,6 +37,7 @@ export function EdgeAgentTab({ onCreate, commands, asyncMode = false }: Props) {
readonly={!!edgeInfo}
key={formKey}
asyncMode={asyncMode}
containerEngine={containerEngine}
/>
{edgeInfo && (

View file

@ -3,12 +3,16 @@ export interface AnalyticsState {
dockerApi: number;
dockerEdgeAgentStandard: number;
dockerEdgeAgentAsync: number;
podmanAgent: number;
podmanEdgeAgentStandard: number;
podmanEdgeAgentAsync: number;
podmanLocalEnvironment: number; // podman socket
kubernetesAgent: number;
kubernetesEdgeAgentStandard: number;
kubernetesEdgeAgentAsync: number;
kaasAgent: number;
aciApi: number;
localEndpoint: number;
localEndpoint: number; // docker socket
}
export type AnalyticsStateKey = keyof AnalyticsState;

View file

@ -3,6 +3,7 @@ import { useMutation } from '@tanstack/react-query';
import { useEnvironmentList } from '@/react/portainer/environments/queries/useEnvironmentList';
import {
ContainerEngine,
Environment,
EnvironmentType,
} from '@/react/portainer/environments/types';
@ -62,11 +63,30 @@ function getStatus(
}
async function createLocalEnvironment() {
try {
return await createLocalKubernetesEnvironment({ name: 'local' });
} catch (err) {
return await createLocalDockerEnvironment({ name: 'local' });
const name = 'local';
const attempts = [
() => createLocalKubernetesEnvironment({ name }),
() =>
createLocalDockerEnvironment({
name,
containerEngine: ContainerEngine.Podman,
}),
() =>
createLocalDockerEnvironment({
name,
containerEngine: ContainerEngine.Docker,
}),
];
for (let i = 0; i < attempts.length; i++) {
try {
return await attempts[i]();
} catch (err) {
// Continue to next attempt
}
}
throw new Error('Failed to create local environment with any method');
}
function useFetchLocalEnvironment() {

View file

@ -1,6 +1,7 @@
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { useIsSwarm } from '@/react/docker/proxy/queries/useInfo';
import { StackType } from '@/react/common/stacks/types';
import { ContainerEngine } from '@/react/portainer/environments/types';
import { PageHeader } from '@@/PageHeader';
import { Widget } from '@@/Widget';
@ -12,16 +13,18 @@ import { CreateForm } from './CreateForm';
export function CreateView() {
const viewType = useViewType();
const environmentId = useEnvironmentId(false);
const isSwarm = useIsSwarm(environmentId, { enabled: viewType === 'docker' });
const isSwarm = useIsSwarm(environmentId, {
enabled: viewType === ContainerEngine.Docker,
});
const defaultType = getDefaultType(viewType, isSwarm);
return (
<div>
<PageHeader
title="Create Custom template"
title="Create Custom Template"
breadcrumbs={[
{ label: 'Custom Templates', link: '^' },
'Create Custom template',
'Create Custom Template',
]}
/>

View file

@ -1,5 +1,6 @@
import { notifySuccess } from '@/portainer/services/notifications';
import { useParamState } from '@/react/hooks/useParamState';
import { ContainerEngine } from '@/react/portainer/environments/types';
import { PageHeader } from '@@/PageHeader';
import { confirmDelete } from '@@/modals/confirm';
@ -28,7 +29,7 @@ export function ListView() {
<>
<PageHeader title="Custom Templates" breadcrumbs="Custom Templates" />
{viewType === 'docker' && !!selectedTemplateId && (
{viewType === ContainerEngine.Docker && !!selectedTemplateId && (
<StackFromCustomTemplateFormWidget templateId={selectedTemplateId} />
)}