mirror of https://github.com/portainer/portainer
refactor(templates): migrate template item to react [EE-6203] (#10429)
parent
d970f0e2bc
commit
1ad9488ca7
@ -1,9 +0,0 @@
|
||||
.template-item-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.template-item-details .template-item-details-sub {
|
||||
width: 100%;
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import './template-item.css';
|
||||
|
||||
angular.module('portainer.app').component('templateItem', {
|
||||
templateUrl: './templateItem.html',
|
||||
bindings: {
|
||||
model: '<',
|
||||
typeLabel: '@',
|
||||
onSelect: '<',
|
||||
},
|
||||
transclude: {
|
||||
actions: '?templateItemActions',
|
||||
},
|
||||
});
|
@ -1,49 +0,0 @@
|
||||
<!-- template -->
|
||||
<div ng-class="{ 'blocklist-item--selected': $ctrl.model.Selected }" class="blocklist-item template-item !my-0 !mr-0" ng-click="$ctrl.onSelect($ctrl.model)">
|
||||
<div class="blocklist-item-box">
|
||||
<!-- template-image -->
|
||||
<div class="vertical-center min-w-[56px] justify-center">
|
||||
<fallback-image src="$ctrl.model.Logo" fallback-icon="'rocket'" class-name="'blocklist-item-logo'" size="'3xl'"></fallback-image>
|
||||
</div>
|
||||
<!-- !template-image -->
|
||||
<!-- template-details -->
|
||||
<div class="col-sm-12 template-item-details">
|
||||
<!-- blocklist-item-line1 -->
|
||||
<div class="blocklist-item-line gap-2">
|
||||
<span class="blocklist-item-title">
|
||||
{{ $ctrl.model.Title }}
|
||||
</span>
|
||||
<div class="space-left blocklist-item-subtitle inline-flex items-center">
|
||||
<div ng-if="$ctrl.typeLabel !== 'manifest'" class="vertical-center gap-1">
|
||||
<pr-icon ng-if="$ctrl.model.Platform === 1 || $ctrl.model.Platform === 'linux' || !$ctrl.model.Platform" icon="'svg-linux'" class="mr-1"></pr-icon>
|
||||
<pr-icon
|
||||
icon="'svg-microsoft'"
|
||||
ng-if="$ctrl.model.Platform === 2 || $ctrl.model.Platform === 'windows' || !$ctrl.model.Platform"
|
||||
class-name="'[&>*]:flex [&>*]:items-center'"
|
||||
size="'lg'"
|
||||
></pr-icon>
|
||||
</div>
|
||||
<!-- currently only kubernetes uses the typeLabel of 'manifest' -->
|
||||
<div ng-if="$ctrl.typeLabel === 'manifest'" class="vertical-center">
|
||||
<pr-icon icon="'svg-kubernetes'" size="'lg'" class="align-bottom" class-name="'[&>*]:flex [&>*]:items-center'"></pr-icon>
|
||||
</div>
|
||||
{{ $ctrl.typeLabel }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- !blocklist-item-line1 -->
|
||||
<span class="blocklist-item-actions" ng-transclude="actions"></span>
|
||||
<!-- blocklist-item-line2 -->
|
||||
<div class="blocklist-item-line template-item-details-sub">
|
||||
<span class="blocklist-item-desc">
|
||||
{{ $ctrl.model.Description }}
|
||||
</span>
|
||||
<span class="small text-muted" ng-if="$ctrl.model.Categories.length > 0">
|
||||
{{ $ctrl.model.Categories.join(', ') }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- !blocklist-item-line2 -->
|
||||
</div>
|
||||
<!-- !template-details -->
|
||||
</div>
|
||||
<!-- !template -->
|
||||
</div>
|
@ -1,107 +0,0 @@
|
||||
import _ from 'lodash-es';
|
||||
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
||||
|
||||
export class TemplateViewModel {
|
||||
constructor(data, version) {
|
||||
switch (version) {
|
||||
case '2':
|
||||
this.setTemplatesV2(data);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported template version');
|
||||
}
|
||||
}
|
||||
|
||||
setTemplatesV2(data) {
|
||||
this.Id = data.Id;
|
||||
this.Title = data.title;
|
||||
this.Type = data.type;
|
||||
this.Description = data.description;
|
||||
this.AdministratorOnly = data.AdministratorOnly;
|
||||
this.Name = data.name;
|
||||
this.Note = data.note;
|
||||
this.Categories = data.categories ? data.categories : [];
|
||||
this.Platform = data.platform ? data.platform : '';
|
||||
this.Logo = data.logo;
|
||||
this.Repository = data.repository;
|
||||
this.Hostname = data.hostname;
|
||||
this.RegistryModel = new PorImageRegistryModel();
|
||||
this.RegistryModel.Image = data.image;
|
||||
this.RegistryModel.Registry.URL = data.registry || '';
|
||||
this.Command = data.command ? data.command : '';
|
||||
this.Network = data.network ? data.network : '';
|
||||
this.Privileged = data.privileged ? data.privileged : false;
|
||||
this.Interactive = data.interactive ? data.interactive : false;
|
||||
this.RestartPolicy = data.restart_policy ? data.restart_policy : 'always';
|
||||
this.Labels = data.labels ? data.labels : [];
|
||||
this.Hosts = data.hosts ? data.hosts : [];
|
||||
this.Env = templateEnv(data);
|
||||
this.Volumes = templateVolumes(data);
|
||||
this.Ports = templatePorts(data);
|
||||
}
|
||||
}
|
||||
|
||||
function templatePorts(data) {
|
||||
var ports = [];
|
||||
|
||||
if (data.ports) {
|
||||
ports = data.ports.map(function (p) {
|
||||
var portAndProtocol = _.split(p, '/');
|
||||
var hostAndContainerPort = _.split(portAndProtocol[0], ':');
|
||||
|
||||
return {
|
||||
hostPort: hostAndContainerPort.length > 1 ? hostAndContainerPort[0] : undefined,
|
||||
containerPort: hostAndContainerPort.length > 1 ? hostAndContainerPort[1] : hostAndContainerPort[0],
|
||||
protocol: portAndProtocol[1],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return ports;
|
||||
}
|
||||
|
||||
function templateVolumes(data) {
|
||||
var volumes = [];
|
||||
|
||||
if (data.volumes) {
|
||||
volumes = data.volumes.map(function (v) {
|
||||
return {
|
||||
container: v.container,
|
||||
readonly: v.readonly || false,
|
||||
type: v.bind ? 'bind' : 'auto',
|
||||
bind: v.bind ? v.bind : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return volumes;
|
||||
}
|
||||
|
||||
function templateEnv(data) {
|
||||
var env = [];
|
||||
|
||||
if (data.env) {
|
||||
env = data.env.map(function (envvar) {
|
||||
envvar.type = 2;
|
||||
envvar.value = envvar.default ? envvar.default : '';
|
||||
|
||||
if (envvar.preset) {
|
||||
envvar.type = 1;
|
||||
}
|
||||
|
||||
if (envvar.select) {
|
||||
envvar.type = 3;
|
||||
for (var i = 0; i < envvar.select.length; i++) {
|
||||
var allowedValue = envvar.select[i];
|
||||
if (allowedValue.default) {
|
||||
envvar.value = allowedValue.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return envvar;
|
||||
});
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import clsx from 'clsx';
|
||||
import { ComponentProps, ComponentType, ElementType } from 'react';
|
||||
|
||||
export type AsComponentProps<E extends ElementType = ElementType> =
|
||||
ComponentProps<E> & {
|
||||
as?: E;
|
||||
};
|
||||
|
||||
export function BlocklistItem<T extends ElementType>({
|
||||
className,
|
||||
isSelected,
|
||||
children,
|
||||
as = 'button',
|
||||
...props
|
||||
}: AsComponentProps & {
|
||||
isSelected?: boolean;
|
||||
as?: ComponentType<T>;
|
||||
}) {
|
||||
const Component = as as 'button';
|
||||
|
||||
return (
|
||||
<Component
|
||||
type="button"
|
||||
className={clsx(
|
||||
className,
|
||||
'blocklist-item flex items-stretch overflow-hidden bg-transparent w-full !ml-0',
|
||||
{
|
||||
'blocklist-item--selected': isSelected,
|
||||
}
|
||||
)}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
import { StackType } from '@/react/common/stacks/types';
|
||||
|
||||
import { ResourceControlResponse } from '../access-control/types';
|
||||
import { RepoConfigResponse } from '../gitops/types';
|
||||
|
||||
export enum Platform {
|
||||
LINUX = 1,
|
||||
WINDOWS,
|
||||
}
|
||||
|
||||
export /**
|
||||
* CustomTemplate represents a custom template.
|
||||
*/
|
||||
interface CustomTemplate {
|
||||
/**
|
||||
* CustomTemplate Identifier.
|
||||
* @example 1
|
||||
*/
|
||||
Id: number;
|
||||
|
||||
/**
|
||||
* Title of the template.
|
||||
* @example "Nginx"
|
||||
*/
|
||||
Title: string;
|
||||
|
||||
/**
|
||||
* Description of the template.
|
||||
* @example "High performance web server"
|
||||
*/
|
||||
Description: string;
|
||||
|
||||
/**
|
||||
* Path on disk to the repository hosting the Stack file.
|
||||
* @example "/data/custom_template/3"
|
||||
*/
|
||||
ProjectPath: string;
|
||||
|
||||
/**
|
||||
* Path to the Stack file.
|
||||
* @example "docker-compose.yml"
|
||||
*/
|
||||
EntryPoint: string;
|
||||
|
||||
/**
|
||||
* User identifier who created this template.
|
||||
* @example 3
|
||||
*/
|
||||
CreatedByUserId: UserId;
|
||||
|
||||
/**
|
||||
* A note that will be displayed in the UI. Supports HTML content.
|
||||
* @example "This is my <b>custom</b> template"
|
||||
*/
|
||||
Note: string;
|
||||
|
||||
/**
|
||||
* Platform associated with the template.
|
||||
* Valid values are: 1 - 'linux', 2 - 'windows'.
|
||||
* @example 1
|
||||
*/
|
||||
Platform: Platform;
|
||||
|
||||
/**
|
||||
* URL of the template's logo.
|
||||
* @example "https://portainer.io/img/logo.svg"
|
||||
*/
|
||||
Logo: string;
|
||||
|
||||
/**
|
||||
* Type of created stack:
|
||||
* - 1: swarm
|
||||
* - 2: compose
|
||||
* - 3: kubernetes
|
||||
* @example 1
|
||||
*/
|
||||
Type: StackType;
|
||||
|
||||
/**
|
||||
* ResourceControl associated with the template.
|
||||
*/
|
||||
ResourceControl?: ResourceControlResponse;
|
||||
|
||||
/**
|
||||
* GitConfig for the template.
|
||||
*/
|
||||
GitConfig?: RepoConfigResponse;
|
||||
|
||||
/**
|
||||
* Indicates if the Kubernetes template is created from a Docker Compose file.
|
||||
* @example false
|
||||
*/
|
||||
IsComposeFormat: boolean;
|
||||
}
|
||||
|
||||
export type CustomTemplateFileContent = {
|
||||
FileContent: string;
|
||||
};
|
||||
|
||||
export const CustomTemplateKubernetesType = 3;
|
||||
|
||||
export enum Types {
|
||||
SWARM = 1,
|
||||
STANDALONE,
|
||||
KUBERNETES,
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { TemplateItem } from '../components/TemplateItem';
|
||||
|
||||
import { TemplateViewModel } from './template';
|
||||
import { TemplateType } from './types';
|
||||
|
||||
export function AppTemplatesListItem({
|
||||
template,
|
||||
onSelect,
|
||||
onDuplicate,
|
||||
isSelected,
|
||||
}: {
|
||||
template: TemplateViewModel;
|
||||
onSelect: (template: TemplateViewModel) => void;
|
||||
onDuplicate: (template: TemplateViewModel) => void;
|
||||
isSelected: boolean;
|
||||
}) {
|
||||
return (
|
||||
<TemplateItem
|
||||
template={template}
|
||||
typeLabel={
|
||||
template.Type === TemplateType.Container ? 'container' : 'stack'
|
||||
}
|
||||
onSelect={() => onSelect(template)}
|
||||
isSelected={isSelected}
|
||||
renderActions={
|
||||
template.Type === TemplateType.SwarmStack ||
|
||||
(template.Type === TemplateType.ComposeStack && (
|
||||
<div className="mr-5 mt-3">
|
||||
<Button
|
||||
size="xsmall"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDuplicate(template);
|
||||
}}
|
||||
>
|
||||
Copy as Custom
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,184 @@
|
||||
import _ from 'lodash';
|
||||
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
||||
|
||||
import { Pair } from '../../settings/types';
|
||||
import { Platform } from '../../custom-templates/types';
|
||||
|
||||
import {
|
||||
AppTemplate,
|
||||
TemplateEnv,
|
||||
TemplateRepository,
|
||||
TemplateType,
|
||||
} from './types';
|
||||
|
||||
export class TemplateViewModel {
|
||||
Id!: string;
|
||||
|
||||
Title!: string;
|
||||
|
||||
Type!: TemplateType;
|
||||
|
||||
Description!: string;
|
||||
|
||||
AdministratorOnly!: boolean;
|
||||
|
||||
Name: string | undefined;
|
||||
|
||||
Note: string | undefined;
|
||||
|
||||
Categories!: string[];
|
||||
|
||||
Platform!: Platform | undefined;
|
||||
|
||||
Logo: string | undefined;
|
||||
|
||||
Repository!: TemplateRepository;
|
||||
|
||||
Hostname: string | undefined;
|
||||
|
||||
RegistryModel!: PorImageRegistryModel;
|
||||
|
||||
Command!: string;
|
||||
|
||||
Network!: string;
|
||||
|
||||
Privileged!: boolean;
|
||||
|
||||
Interactive!: boolean;
|
||||
|
||||
RestartPolicy!: string;
|
||||
|
||||
Labels!: Pair[];
|
||||
|
||||
Env!: Array<TemplateEnv & { type: EnvVarType; value: string }>;
|
||||
|
||||
Volumes!: {
|
||||
container: string;
|
||||
readonly: boolean;
|
||||
type: string;
|
||||
bind: string | null;
|
||||
}[];
|
||||
|
||||
Ports!: {
|
||||
hostPort: string | undefined;
|
||||
containerPort: string;
|
||||
protocol: string;
|
||||
}[];
|
||||
|
||||
constructor(data: AppTemplate, version: string) {
|
||||
switch (version) {
|
||||
case '2':
|
||||
this.setTemplatesV2(data);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported template version');
|
||||
}
|
||||
}
|
||||
|
||||
setTemplatesV2(template: AppTemplate) {
|
||||
this.Id = _.uniqueId();
|
||||
this.Title = template.title;
|
||||
this.Type = template.type;
|
||||
this.Description = template.description;
|
||||
this.AdministratorOnly = template.administrator_only;
|
||||
this.Name = template.name;
|
||||
this.Note = template.note;
|
||||
this.Categories = template.categories ? template.categories : [];
|
||||
this.Platform = getPlatform(template.platform);
|
||||
this.Logo = template.logo;
|
||||
this.Repository = template.repository;
|
||||
this.Hostname = template.hostname;
|
||||
this.RegistryModel = new PorImageRegistryModel();
|
||||
this.RegistryModel.Image = template.image;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.RegistryModel.Registry.URL = template.registry || '';
|
||||
this.Command = template.command ? template.command : '';
|
||||
this.Network = template.network ? template.network : '';
|
||||
this.Privileged = template.privileged ? template.privileged : false;
|
||||
this.Interactive = template.interactive ? template.interactive : false;
|
||||
this.RestartPolicy = template.restart_policy
|
||||
? template.restart_policy
|
||||
: 'always';
|
||||
this.Labels = template.labels ? template.labels : [];
|
||||
this.Env = templateEnv(template);
|
||||
this.Volumes = templateVolumes(template);
|
||||
this.Ports = templatePorts(template);
|
||||
}
|
||||
}
|
||||
|
||||
function templatePorts(data: AppTemplate) {
|
||||
return (
|
||||
data.ports?.map((p) => {
|
||||
const portAndProtocol = _.split(p, '/');
|
||||
const hostAndContainerPort = _.split(portAndProtocol[0], ':');
|
||||
|
||||
return {
|
||||
hostPort:
|
||||
hostAndContainerPort.length > 1 ? hostAndContainerPort[0] : undefined,
|
||||
containerPort:
|
||||
hostAndContainerPort.length > 1
|
||||
? hostAndContainerPort[1]
|
||||
: hostAndContainerPort[0],
|
||||
protocol: portAndProtocol[1],
|
||||
};
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
|
||||
function templateVolumes(data: AppTemplate) {
|
||||
return (
|
||||
data.volumes?.map((v) => ({
|
||||
container: v.container,
|
||||
readonly: v.readonly || false,
|
||||
type: v.bind ? 'bind' : 'auto',
|
||||
bind: v.bind ? v.bind : null,
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
|
||||
enum EnvVarType {
|
||||
PreSelected = 1,
|
||||
Text = 2,
|
||||
Select = 3,
|
||||
}
|
||||
|
||||
function templateEnv(data: AppTemplate) {
|
||||
return (
|
||||
data.env?.map((envvar) => ({
|
||||
name: envvar.name,
|
||||
label: envvar.label,
|
||||
description: envvar.description,
|
||||
default: envvar.default,
|
||||
preset: envvar.preset,
|
||||
select: envvar.select,
|
||||
...getEnvVarTypeAndValue(envvar),
|
||||
})) || []
|
||||
);
|
||||
|
||||
function getEnvVarTypeAndValue(envvar: TemplateEnv) {
|
||||
if (envvar.select) {
|
||||
return {
|
||||
type: EnvVarType.Select,
|
||||
value: envvar.select.find((v) => v.default)?.value || '',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: envvar.preset ? EnvVarType.PreSelected : EnvVarType.Text,
|
||||
value: envvar.default || '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getPlatform(platform?: 'linux' | 'windows' | '') {
|
||||
switch (platform) {
|
||||
case 'linux':
|
||||
return Platform.LINUX;
|
||||
case 'windows':
|
||||
return Platform.WINDOWS;
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
@ -0,0 +1,250 @@
|
||||
import { Pair } from '../../settings/types';
|
||||
|
||||
export enum TemplateType {
|
||||
Container = 1,
|
||||
SwarmStack = 2,
|
||||
ComposeStack = 3,
|
||||
ComposeEdgeStack = 4,
|
||||
}
|
||||
|
||||
/**
|
||||
* Template represents an application template that can be used as an App Template or an Edge template.
|
||||
*/
|
||||
export interface AppTemplate {
|
||||
/**
|
||||
* Template type. Valid values are: 1 (container), 2 (Swarm stack), 3 (Compose stack), 4 (Compose edge stack).
|
||||
* @example 1
|
||||
*/
|
||||
type: TemplateType;
|
||||
|
||||
/**
|
||||
* Title of the template.
|
||||
* @example "Nginx"
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* Description of the template.
|
||||
* @example "High performance web server"
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* Whether the template should be available to administrators only.
|
||||
* @example true
|
||||
*/
|
||||
administrator_only: boolean;
|
||||
|
||||
/**
|
||||
* Image associated with a container template. Mandatory for a container template.
|
||||
* @example "nginx:latest"
|
||||
*/
|
||||
image: string;
|
||||
|
||||
/**
|
||||
* Repository associated with the template.
|
||||
*/
|
||||
repository: TemplateRepository;
|
||||
|
||||
/**
|
||||
* Stack file used for this template (Mandatory for Edge stack).
|
||||
*/
|
||||
stackFile?: string;
|
||||
|
||||
/**
|
||||
* Default name for the stack/container to be used on deployment.
|
||||
* @example "mystackname"
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* URL of the template's logo.
|
||||
* @example "https://portainer.io/img/logo.svg"
|
||||
*/
|
||||
logo?: string;
|
||||
|
||||
/**
|
||||
* A list of environment (endpoint) variables used during the template deployment.
|
||||
*/
|
||||
env?: TemplateEnv[];
|
||||
|
||||
/**
|
||||
* A note that will be displayed in the UI. Supports HTML content.
|
||||
* @example "This is my <b>custom</b> template"
|
||||
*/
|
||||
note?: string;
|
||||
|
||||
/**
|
||||
* Platform associated with the template.
|
||||
* Valid values are: 'linux', 'windows' or leave empty for multi-platform.
|
||||
* @example "linux"
|
||||
*/
|
||||
platform?: 'linux' | 'windows' | '';
|
||||
|
||||
/**
|
||||
* A list of categories associated with the template.
|
||||
* @example ["database"]
|
||||
*/
|
||||
categories?: string[];
|
||||
|
||||
/**
|
||||
* The URL of a registry associated with the image for a container template.
|
||||
* @example "quay.io"
|
||||
*/
|
||||
registry?: string;
|
||||
|
||||
/**
|
||||
* The command that will be executed in a container template.
|
||||
* @example "ls -lah"
|
||||
*/
|
||||
command?: string;
|
||||
|
||||
/**
|
||||
* Name of a network that will be used on container deployment if it exists inside the environment (endpoint).
|
||||
* @example "mynet"
|
||||
*/
|
||||
network?: string;
|
||||
|
||||
/**
|
||||
* A list of volumes used during the container template deployment.
|
||||
*/
|
||||
volumes?: TemplateVolume[];
|
||||
|
||||
/**
|
||||
* A list of ports exposed by the container.
|
||||
* @example ["8080:80/tcp"]
|
||||
*/
|
||||
ports?: string[];
|
||||
|
||||
/**
|
||||
* Container labels.
|
||||
*/
|
||||
labels?: Pair[];
|
||||
|
||||
/**
|
||||
* Whether the container should be started in privileged mode.
|
||||
* @example true
|
||||
*/
|
||||
privileged?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the container should be started in interactive mode (-i -t equivalent on the CLI).
|
||||
* @example true
|
||||
*/
|
||||
interactive?: boolean;
|
||||
|
||||
/**
|
||||
* Container restart policy.
|
||||
* @example "on-failure"
|
||||
*/
|
||||
restart_policy?: string;
|
||||
|
||||
/**
|
||||
* Container hostname.
|
||||
* @example "mycontainer"
|
||||
*/
|
||||
hostname?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TemplateRepository represents the git repository configuration for a template.
|
||||
*/
|
||||
export interface TemplateRepository {
|
||||
/**
|
||||
* URL of a git repository used to deploy a stack template. Mandatory for a Swarm/Compose stack template.
|
||||
* @example "https://github.com/portainer/portainer-compose"
|
||||
*/
|
||||
url: string;
|
||||
|
||||
/**
|
||||
* Path to the stack file inside the git repository.
|
||||
* @example "./subfolder/docker-compose.yml"
|
||||
*/
|
||||
stackfile: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TemplateVolume represents a template volume configuration.
|
||||
*/
|
||||
interface TemplateVolume {
|
||||
/**
|
||||
* Path inside the container.
|
||||
* @example "/data"
|
||||
*/
|
||||
container: string;
|
||||
|
||||
/**
|
||||
* Path on the host.
|
||||
* @example "/tmp"
|
||||
*/
|
||||
bind?: string;
|
||||
|
||||
/**
|
||||
* Whether the volume used should be readonly.
|
||||
* @example true
|
||||
*/
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* TemplateEnv represents an environment (endpoint) variable for a template.
|
||||
*/
|
||||
export interface TemplateEnv {
|
||||
/**
|
||||
* Name of the environment (endpoint) variable.
|
||||
* @example "MYSQL_ROOT_PASSWORD"
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Text for the label that will be generated in the UI.
|
||||
* @example "Root password"
|
||||
*/
|
||||
label?: string;
|
||||
|
||||
/**
|
||||
* Content of the tooltip that will be generated in the UI.
|
||||
* @example "MySQL root account password"
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* Default value that will be set for the variable.
|
||||
* @example "default_value"
|
||||
*/
|
||||
default?: string;
|
||||
|
||||
/**
|
||||
* If set to true, will not generate any input for this variable in the UI.
|
||||
* @example false
|
||||
*/
|
||||
preset?: boolean;
|
||||
|
||||
/**
|
||||
* A list of name/value pairs that will be used to generate a dropdown in the UI.
|
||||
*/
|
||||
select?: TemplateEnvSelect[];
|
||||
}
|
||||
|
||||
/**
|
||||
* TemplateEnvSelect represents a text/value pair that will be displayed as a choice for the template user.
|
||||
*/
|
||||
interface TemplateEnvSelect {
|
||||
/**
|
||||
* Some text that will be displayed as a choice.
|
||||
* @example "text value"
|
||||
*/
|
||||
text: string;
|
||||
|
||||
/**
|
||||
* A value that will be associated with the choice.
|
||||
* @example "value"
|
||||
*/
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* Will set this choice as the default choice.
|
||||
* @example false
|
||||
*/
|
||||
default: boolean;
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import LinuxIcon from '@/assets/ico/linux.svg?c';
|
||||
import MicrosoftIcon from '@/assets/ico/vendor/microsoft.svg?c';
|
||||
import KubernetesIcon from '@/assets/ico/vendor/kubernetes.svg?c';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
import { FallbackImage } from '@@/FallbackImage';
|
||||
import { BlocklistItem } from '@@/Blocklist/BlocklistItem';
|
||||
|
||||
import { Platform } from '../../custom-templates/types';
|
||||
|
||||
type Value = {
|
||||
Id: number | string;
|
||||
Logo?: string;
|
||||
Title: string;
|
||||
Platform?: Platform;
|
||||
Description: string;
|
||||
Categories?: string[];
|
||||
};
|
||||
|
||||
export function TemplateItem({
|
||||
template,
|
||||
typeLabel,
|
||||
onSelect,
|
||||
renderActions,
|
||||
isSelected,
|
||||
}: {
|
||||
template: Value;
|
||||
typeLabel: string;
|
||||
onSelect: () => void;
|
||||
renderActions: ReactNode;
|
||||
isSelected: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<BlocklistItem isSelected={isSelected} onClick={() => onSelect()}>
|
||||
<div className="vertical-center min-w-[56px] justify-center">
|
||||
<FallbackImage
|
||||
src={template.Logo}
|
||||
fallbackIcon="rocket"
|
||||
className="blocklist-item-logo"
|
||||
size="3xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-12 flex justify-between flex-wrap">
|
||||
<div className="blocklist-item-line gap-2">
|
||||
<span className="blocklist-item-title">{template.Title}</span>
|
||||
<div className="space-left blocklist-item-subtitle inline-flex items-center">
|
||||
<div className="vertical-center gap-1">
|
||||
{(template.Platform === Platform.LINUX ||
|
||||
!template.Platform) && (
|
||||
<Icon icon={LinuxIcon} className="mr-1" />
|
||||
)}
|
||||
{(template.Platform === Platform.WINDOWS ||
|
||||
!template.Platform) && (
|
||||
<Icon
|
||||
icon={MicrosoftIcon}
|
||||
className="[&>*]:flex [&>*]:items-center"
|
||||
size="lg"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{typeLabel === 'manifest' && (
|
||||
<div className="vertical-center">
|
||||
<Icon
|
||||
icon={KubernetesIcon}
|
||||
size="lg"
|
||||
className="align-bottom [&>*]:flex [&>*]:items-center"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{typeLabel}
|
||||
</div>
|
||||
</div>
|
||||
<div className="blocklist-item-line w-full">
|
||||
<span className="blocklist-item-desc">{template.Description}</span>
|
||||
{template.Categories && template.Categories.length > 0 && (
|
||||
<span className="small text-muted">
|
||||
{template.Categories.join(', ')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BlocklistItem>
|
||||
<span className="absolute inset-y-0 right-0 justify-end">
|
||||
{renderActions}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import { Edit, Trash2 } from 'lucide-react';
|
||||
|
||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||
import { StackType } from '@/react/common/stacks/types';
|
||||
import { CustomTemplate } from '@/react/portainer/custom-templates/types';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
import { Link } from '@@/Link';
|
||||
|
||||
import { TemplateItem } from '../../components/TemplateItem';
|
||||
|
||||
export function CustomTemplatesListItem({
|
||||
template,
|
||||
onSelect,
|
||||
onDelete,
|
||||
isSelected,
|
||||
}: {
|
||||
template: CustomTemplate;
|
||||
onSelect: (templateId: CustomTemplate['Id']) => void;
|
||||
onDelete: (templateId: CustomTemplate['Id']) => void;
|
||||
isSelected: boolean;
|
||||
}) {
|
||||
const { isAdmin, user } = useCurrentUser();
|
||||
const isEditAllowed = isAdmin || template.CreatedByUserId === user.Id;
|
||||
|
||||
return (
|
||||
<TemplateItem
|
||||
template={template}
|
||||
typeLabel={getTypeLabel(template.Type)}
|
||||
onSelect={() => onSelect(template.Id)}
|
||||
isSelected={isSelected}
|
||||
renderActions={
|
||||
<div className="mr-4 mt-3">
|
||||
{isEditAllowed && (
|
||||
<div className="vertical-center">
|
||||
<Button
|
||||
as={Link}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
color="secondary"
|
||||
props={{
|
||||
to: '.edit',
|
||||
params: {
|
||||
id: template.Id,
|
||||
},
|
||||
}}
|
||||
icon={Edit}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
onDelete(template.Id);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
color="dangerlight"
|
||||
icon={Trash2}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getTypeLabel(type: StackType) {
|
||||
switch (type) {
|
||||
case StackType.DockerSwarm:
|
||||
return 'swarm';
|
||||
case StackType.Kubernetes:
|
||||
return 'manifest';
|
||||
case StackType.DockerCompose:
|
||||
default:
|
||||
return 'standalone';
|
||||
}
|
||||
}
|
Loading…
Reference in new issue