1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-24 07:49:41 +02:00

refactor(templates): migrate list view to react [EE-2296] (#10999)

This commit is contained in:
Chaim Lev-Ari 2024-04-11 09:29:30 +03:00 committed by GitHub
parent d38085a560
commit 6ff4fd3db2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
103 changed files with 2628 additions and 1315 deletions

View file

@ -0,0 +1,14 @@
import { buildStackUrl } from '../buildUrl';
export function buildCreateUrl(
stackType: 'kubernetes',
method: 'repository' | 'url' | 'string'
): string;
export function buildCreateUrl(
stackType: 'swarm' | 'standalone',
method: 'repository' | 'string' | 'file'
): string;
export function buildCreateUrl(stackType: string, method: string) {
return buildStackUrl(undefined, `create/${stackType}/${method}`);
}

View file

@ -0,0 +1,36 @@
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Stack } from '../../types';
import { buildCreateUrl } from './buildUrl';
export interface KubernetesFileContentPayload {
/** Name of the stack */
stackName: string;
/** Content of the Stack file */
stackFileContent: string;
composeFormat: boolean;
namespace: string;
/** Whether the stack is from an app template */
fromAppTemplate?: boolean;
environmentId: EnvironmentId;
}
export async function createKubernetesStackFromFileContent({
environmentId,
...payload
}: KubernetesFileContentPayload) {
try {
const { data } = await axios.post<Stack>(
buildCreateUrl('kubernetes', 'string'),
payload,
{
params: { endpointId: environmentId },
}
);
return data;
} catch (e) {
throw parseAxiosError(e as Error);
}
}

View file

@ -0,0 +1,54 @@
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { AutoUpdateModel } from '@/react/portainer/gitops/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Stack } from '../../types';
import { buildCreateUrl } from './buildUrl';
export type KubernetesGitRepositoryPayload = {
/** Name of the stack */
stackName: string;
composeFormat: boolean;
namespace: string;
/** URL of a Git repository hosting the Stack file */
repositoryUrl: string;
/** Reference name of a Git repository hosting the Stack file */
repositoryReferenceName?: string;
/** Use basic authentication to clone the Git repository */
repositoryAuthentication?: boolean;
/** Username used in basic authentication. Required when RepositoryAuthentication is true. */
repositoryUsername?: string;
/** Password used in basic authentication. Required when RepositoryAuthentication is true. */
repositoryPassword?: string;
/** GitCredentialID used to identify the binded git credential */
repositoryGitCredentialId?: number;
/** Path to the Stack file inside the Git repository */
manifestFile?: string;
additionalFiles?: Array<string>;
/** TLSSkipVerify skips SSL verification when cloning the Git repository */
tlsSkipVerify?: boolean;
/** Optional GitOps update configuration */
autoUpdate?: AutoUpdateModel;
environmentId: EnvironmentId;
};
export async function createKubernetesStackFromGit({
environmentId,
...payload
}: KubernetesGitRepositoryPayload) {
try {
const { data } = await axios.post<Stack>(
buildCreateUrl('kubernetes', 'repository'),
payload,
{
params: { endpointId: environmentId },
}
);
return data;
} catch (e) {
throw parseAxiosError(e as Error);
}
}

View file

@ -0,0 +1,32 @@
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Stack } from '../../types';
import { buildCreateUrl } from './buildUrl';
export interface KubernetesUrlPayload {
stackName: string;
composeFormat: boolean;
namespace: string;
manifestURL: string;
environmentId: EnvironmentId;
}
export async function createKubernetesStackFromUrl({
environmentId,
...payload
}: KubernetesUrlPayload) {
try {
const { data } = await axios.post<Stack>(
buildCreateUrl('kubernetes', 'url'),
payload,
{
params: { endpointId: environmentId },
}
);
return data;
} catch (e) {
throw parseAxiosError(e as Error);
}
}

View file

@ -0,0 +1,44 @@
import axios, {
json2formData,
parseAxiosError,
} from '@/portainer/services/axios';
import { Pair } from '@/react/portainer/settings/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Stack } from '../../types';
import { buildCreateUrl } from './buildUrl';
export type StandaloneFileUploadPayload = {
/** Name of the stack */
Name: string;
file: File;
/** List of environment variables */
Env?: Array<Pair>;
/** A UUID to identify a webhook. The stack will be force updated and pull the latest image when the webhook was invoked. */
Webhook?: string;
environmentId: EnvironmentId;
};
export async function createStandaloneStackFromFile({
environmentId,
...payload
}: StandaloneFileUploadPayload) {
try {
const { data } = await axios.post<Stack>(
buildCreateUrl('standalone', 'file'),
json2formData(payload),
{
headers: {
'Content-Type': 'multipart/form-data',
},
params: { endpointId: environmentId },
}
);
return data;
} catch (e) {
throw parseAxiosError(e as Error);
}
}

View file

@ -0,0 +1,40 @@
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { Pair } from '@/react/portainer/settings/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Stack } from '../../types';
import { buildCreateUrl } from './buildUrl';
export interface StandaloneFileContentPayload {
/** Name of the stack */
name: string;
stackFileContent: string;
/** List of environment variables */
env?: Array<Pair>;
/** Whether the stack is from an app template */
fromAppTemplate?: boolean;
/** A UUID to identify a webhook. The stack will be force updated and pull the latest image when the webhook was invoked. */
webhook?: string;
environmentId: EnvironmentId;
}
export async function createStandaloneStackFromFileContent({
environmentId,
...payload
}: StandaloneFileContentPayload) {
try {
const { data } = await axios.post<Stack>(
buildCreateUrl('standalone', 'string'),
payload,
{
params: { endpointId: environmentId },
}
);
return data;
} catch (e) {
throw parseAxiosError(e as Error);
}
}

View file

@ -0,0 +1,63 @@
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { Pair } from '@/react/portainer/settings/types';
import { AutoUpdateModel } from '@/react/portainer/gitops/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Stack } from '../../types';
import { buildCreateUrl } from './buildUrl';
export type StandaloneGitRepositoryPayload = {
/** Name of the stack */
name: string;
/** List of environment variables */
env?: Array<Pair>;
/** Whether the stack is from an app template */
fromAppTemplate?: boolean;
/** URL of a Git repository hosting the Stack file */
repositoryUrl: string;
/** Reference name of a Git repository hosting the Stack file */
repositoryReferenceName?: string;
/** Use basic authentication to clone the Git repository */
repositoryAuthentication?: boolean;
/** Username used in basic authentication. Required when RepositoryAuthentication is true. */
repositoryUsername?: string;
/** Password used in basic authentication. Required when RepositoryAuthentication is true. */
repositoryPassword?: string;
/** GitCredentialID used to identify the binded git credential */
repositoryGitCredentialId?: number;
/** Path to the Stack file inside the Git repository */
composeFile?: string;
additionalFiles?: Array<string>;
/** Optional GitOps update configuration */
autoUpdate?: AutoUpdateModel;
/** Whether the stack supports relative path volume */
supportRelativePath?: boolean;
/** Local filesystem path */
filesystemPath?: string;
/** TLSSkipVerify skips SSL verification when cloning the Git repository */
tlsSkipVerify?: boolean;
environmentId: EnvironmentId;
};
export async function createStandaloneStackFromGit({
environmentId,
...payload
}: StandaloneGitRepositoryPayload) {
try {
const { data } = await axios.post<Stack>(
buildCreateUrl('standalone', 'repository'),
payload,
{
params: { endpointId: environmentId },
}
);
return data;
} catch (e) {
throw parseAxiosError(e as Error);
}
}

View file

@ -0,0 +1,50 @@
import axios, {
json2formData,
parseAxiosError,
} from '@/portainer/services/axios';
import { Pair } from '@/react/portainer/settings/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Stack } from '../../types';
import { buildCreateUrl } from './buildUrl';
export type SwarmFileUploadPayload = {
/** Name of the stack */
Name: string;
/** List of environment variables */
Env?: Array<Pair>;
/** A UUID to identify a webhook. The stack will be force updated and pull the latest image when the webhook was invoked. */
Webhook?: string;
/** Swarm cluster identifier */
SwarmID: string;
file: File;
environmentId: EnvironmentId;
};
export async function createSwarmStackFromFile({
environmentId,
...payload
}: SwarmFileUploadPayload) {
try {
const { data } = await axios.post<Stack>(
buildCreateUrl('swarm', 'file'),
json2formData(payload),
{
headers: {
'Content-Type': 'multipart/form-data',
},
params: {
endpointId: environmentId,
},
}
);
return data;
} catch (e) {
throw parseAxiosError(e as Error);
}
}

View file

@ -0,0 +1,43 @@
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { Pair } from '@/react/portainer/settings/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Stack } from '../../types';
import { buildCreateUrl } from './buildUrl';
export interface SwarmFileContentPayload {
/** Name of the stack */
name: string;
stackFileContent: string;
/** List of environment variables */
env?: Array<Pair>;
/** Whether the stack is from an app template */
fromAppTemplate?: boolean;
/** A UUID to identify a webhook. The stack will be force updated and pull the latest image when the webhook was invoked. */
webhook?: string;
/** Swarm cluster identifier */
swarmID: string;
environmentId: EnvironmentId;
}
export async function createSwarmStackFromFileContent({
environmentId,
...payload
}: SwarmFileContentPayload) {
try {
const { data } = await axios.post<Stack>(
buildCreateUrl('swarm', 'string'),
payload,
{
params: { endpointId: environmentId },
}
);
return data;
} catch (e) {
throw parseAxiosError(e as Error);
}
}

View file

@ -0,0 +1,65 @@
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { Pair } from '@/react/portainer/settings/types';
import { AutoUpdateModel } from '@/react/portainer/gitops/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Stack } from '../../types';
import { buildCreateUrl } from './buildUrl';
export type SwarmGitRepositoryPayload = {
/** Name of the stack */
name: string;
/** List of environment variables */
env?: Array<Pair>;
/** Whether the stack is from an app template */
fromAppTemplate?: boolean;
/** Swarm cluster identifier */
swarmID: string;
/** URL of a Git repository hosting the Stack file */
repositoryUrl: string;
/** Reference name of a Git repository hosting the Stack file */
repositoryReferenceName?: string;
/** Use basic authentication to clone the Git repository */
repositoryAuthentication?: boolean;
/** Username used in basic authentication. Required when RepositoryAuthentication is true. */
repositoryUsername?: string;
/** Password used in basic authentication. Required when RepositoryAuthentication is true. */
repositoryPassword?: string;
/** GitCredentialID used to identify the binded git credential */
repositoryGitCredentialId?: number;
/** Path to the Stack file inside the Git repository */
composeFile?: string;
additionalFiles?: Array<string>;
/** Optional GitOps update configuration */
autoUpdate?: AutoUpdateModel;
/** Whether the stack supports relative path volume */
supportRelativePath?: boolean;
/** Local filesystem path */
filesystemPath?: string;
/** TLSSkipVerify skips SSL verification when cloning the Git repository */
tlsSkipVerify?: boolean;
environmentId: EnvironmentId;
};
export async function createSwarmStackFromGit({
environmentId,
...payload
}: SwarmGitRepositoryPayload) {
try {
const { data } = await axios.post<Stack>(
buildCreateUrl('standalone', 'repository'),
payload,
{
params: { endpointId: environmentId },
}
);
return data;
} catch (e) {
throw parseAxiosError(e as Error);
}
}

View file

@ -0,0 +1,302 @@
import { useMutation, useQueryClient } from 'react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Pair } from '@/react/portainer/settings/types';
import {
GitFormModel,
RelativePathModel,
} from '@/react/portainer/gitops/types';
import { applyResourceControl } from '@/react/portainer/access-control/access-control.service';
import { AccessControlFormData } from '@/react/portainer/access-control/types';
import PortainerError from '@/portainer/error';
import { withError, withInvalidate } from '@/react-tools/react-query';
import { queryKeys } from '../query-keys';
import { createSwarmStackFromFile } from './createSwarmStackFromFile';
import { createSwarmStackFromGit } from './createSwarmStackFromGit';
import { createSwarmStackFromFileContent } from './createSwarmStackFromFileContent';
import { createStandaloneStackFromFile } from './createStandaloneStackFromFile';
import { createStandaloneStackFromGit } from './createStandaloneStackFromGit';
import { createStandaloneStackFromFileContent } from './createStandaloneStackFromFileContent';
import { createKubernetesStackFromUrl } from './createKubernetesStackFromUrl';
import { createKubernetesStackFromGit } from './createKubernetesStackFromGit';
import { createKubernetesStackFromFileContent } from './createKubernetesStackFromFileContent';
export function useCreateStack() {
const queryClient = useQueryClient();
return useMutation(createStack, {
...withError('Failed to create stack'),
...withInvalidate(queryClient, [queryKeys.base()]),
});
}
type BasePayload = {
name: string;
environmentId: EnvironmentId;
};
type DockerBasePayload = BasePayload & {
env?: Array<Pair>;
accessControl: AccessControlFormData;
};
type SwarmBasePayload = DockerBasePayload & {
swarmId: string;
};
type KubernetesBasePayload = BasePayload & {
namespace: string;
composeFormat: boolean;
};
export type SwarmCreatePayload =
| {
method: 'file';
payload: SwarmBasePayload & {
/** File to upload */
file: File;
/** Optional webhook configuration */
webhook?: string;
};
}
| {
method: 'string';
payload: SwarmBasePayload & {
/** Content of the Stack file */
fileContent: string;
/** Optional webhook configuration */
webhook?: string;
fromAppTemplate?: boolean;
};
}
| {
method: 'git';
payload: SwarmBasePayload & {
git: GitFormModel;
relativePathSettings?: RelativePathModel;
fromAppTemplate?: boolean;
};
};
type StandaloneCreatePayload =
| {
method: 'file';
payload: DockerBasePayload & {
/** File to upload */
file: File;
/** Optional webhook configuration */
webhook?: string;
};
}
| {
method: 'string';
payload: DockerBasePayload & {
/** Content of the Stack file */
fileContent: string;
/** Optional webhook configuration */
webhook?: string;
fromAppTemplate?: boolean;
};
}
| {
method: 'git';
payload: DockerBasePayload & {
git: GitFormModel;
relativePathSettings?: RelativePathModel;
fromAppTemplate?: boolean;
};
};
type KubernetesCreatePayload =
| {
method: 'string';
payload: KubernetesBasePayload & {
/** Content of the Stack file */
fileContent: string;
/** Optional webhook configuration */
webhook?: string;
};
}
| {
method: 'git';
payload: KubernetesBasePayload & {
git: GitFormModel;
relativePathSettings?: RelativePathModel;
};
}
| {
method: 'url';
payload: KubernetesBasePayload & {
manifestUrl: string;
};
};
export type CreateStackPayload =
| ({ type: 'swarm' } & SwarmCreatePayload)
| ({ type: 'standalone' } & StandaloneCreatePayload)
| ({ type: 'kubernetes' } & KubernetesCreatePayload);
async function createStack(payload: CreateStackPayload) {
const stack = await createActualStack(payload);
if (payload.type === 'standalone' || payload.type === 'swarm') {
const resourceControl = stack.ResourceControl;
// Portainer will always return a resource control, but since types mark it as optional, we need to check it.
// Ignoring the missing value will result with bugs, hence it's better to throw an error
if (!resourceControl) {
throw new PortainerError('resource control expected after creation');
}
await applyResourceControl(
payload.payload.accessControl,
resourceControl.Id
);
}
}
function createActualStack(payload: CreateStackPayload) {
switch (payload.type) {
case 'swarm':
return createSwarmStack(payload);
case 'standalone':
return createStandaloneStack(payload);
case 'kubernetes':
return createKubernetesStack(payload);
default:
throw new Error('Invalid type');
}
}
function createSwarmStack({ method, payload }: SwarmCreatePayload) {
switch (method) {
case 'file':
return createSwarmStackFromFile({
environmentId: payload.environmentId,
file: payload.file,
Name: payload.name,
SwarmID: payload.swarmId,
Env: payload.env,
Webhook: payload.webhook,
});
case 'git':
return createSwarmStackFromGit({
name: payload.name,
env: payload.env,
repositoryUrl: payload.git.RepositoryURL,
repositoryReferenceName: payload.git.RepositoryReferenceName,
composeFile: payload.git.ComposeFilePathInRepository,
repositoryAuthentication: payload.git.RepositoryAuthentication,
repositoryUsername: payload.git.RepositoryUsername,
repositoryPassword: payload.git.RepositoryPassword,
repositoryGitCredentialId: payload.git.RepositoryGitCredentialID,
filesystemPath: payload.relativePathSettings?.FilesystemPath,
supportRelativePath: payload.relativePathSettings?.SupportRelativePath,
tlsSkipVerify: payload.git.TLSSkipVerify,
autoUpdate: payload.git.AutoUpdate,
environmentId: payload.environmentId,
swarmID: payload.swarmId,
additionalFiles: payload.git.AdditionalFiles,
fromAppTemplate: payload.fromAppTemplate,
});
case 'string':
return createSwarmStackFromFileContent({
name: payload.name,
env: payload.env,
environmentId: payload.environmentId,
stackFileContent: payload.fileContent,
webhook: payload.webhook,
swarmID: payload.swarmId,
fromAppTemplate: payload.fromAppTemplate,
});
default:
throw new Error('Invalid method');
}
}
function createStandaloneStack({ method, payload }: StandaloneCreatePayload) {
switch (method) {
case 'file':
return createStandaloneStackFromFile({
environmentId: payload.environmentId,
file: payload.file,
Name: payload.name,
Env: payload.env,
Webhook: payload.webhook,
});
case 'git':
return createStandaloneStackFromGit({
name: payload.name,
env: payload.env,
repositoryUrl: payload.git.RepositoryURL,
repositoryReferenceName: payload.git.RepositoryReferenceName,
composeFile: payload.git.ComposeFilePathInRepository,
repositoryAuthentication: payload.git.RepositoryAuthentication,
repositoryUsername: payload.git.RepositoryUsername,
repositoryPassword: payload.git.RepositoryPassword,
repositoryGitCredentialId: payload.git.RepositoryGitCredentialID,
filesystemPath: payload.relativePathSettings?.FilesystemPath,
supportRelativePath: payload.relativePathSettings?.SupportRelativePath,
tlsSkipVerify: payload.git.TLSSkipVerify,
autoUpdate: payload.git.AutoUpdate,
environmentId: payload.environmentId,
additionalFiles: payload.git.AdditionalFiles,
fromAppTemplate: payload.fromAppTemplate,
});
case 'string':
return createStandaloneStackFromFileContent({
name: payload.name,
env: payload.env,
environmentId: payload.environmentId,
stackFileContent: payload.fileContent,
webhook: payload.webhook,
fromAppTemplate: payload.fromAppTemplate,
});
default:
throw new Error('Invalid method');
}
}
function createKubernetesStack({ method, payload }: KubernetesCreatePayload) {
switch (method) {
case 'string':
return createKubernetesStackFromFileContent({
stackName: payload.name,
environmentId: payload.environmentId,
stackFileContent: payload.fileContent,
composeFormat: payload.composeFormat,
namespace: payload.namespace,
});
case 'git':
return createKubernetesStackFromGit({
stackName: payload.name,
repositoryUrl: payload.git.RepositoryURL,
repositoryReferenceName: payload.git.RepositoryReferenceName,
manifestFile: payload.git.ComposeFilePathInRepository,
repositoryAuthentication: payload.git.RepositoryAuthentication,
repositoryUsername: payload.git.RepositoryUsername,
repositoryPassword: payload.git.RepositoryPassword,
repositoryGitCredentialId: payload.git.RepositoryGitCredentialID,
tlsSkipVerify: payload.git.TLSSkipVerify,
autoUpdate: payload.git.AutoUpdate,
environmentId: payload.environmentId,
additionalFiles: payload.git.AdditionalFiles,
composeFormat: payload.composeFormat,
namespace: payload.namespace,
});
case 'url':
return createKubernetesStackFromUrl({
stackName: payload.name,
composeFormat: payload.composeFormat,
environmentId: payload.environmentId,
manifestURL: payload.manifestUrl,
namespace: payload.namespace,
});
default:
throw new Error('Invalid method');
}
}