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

refactor(app): migrate remaining form sections [EE-6231] (#10938)
Some checks are pending
ci / build_images (map[arch:amd64 platform:linux]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
ci / build_images (map[arch:arm64 platform:linux]) (push) Waiting to run
ci / build_manifests (push) Blocked by required conditions
/ triage (push) Waiting to run
Lint / Run linters (push) Waiting to run
Test / test-client (push) Waiting to run
Test / test-server (map[arch:amd64 platform:linux]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
Test / test-server (map[arch:arm64 platform:linux]) (push) Waiting to run

This commit is contained in:
Ali 2024-01-11 15:13:28 +13:00 committed by GitHub
parent 0b9cebc685
commit 4e7d1c7088
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 456 additions and 284 deletions

View file

@ -0,0 +1,102 @@
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { useEnvironmentDeploymentOptions } from '@/react/portainer/environments/queries/useEnvironment';
import { useAuthorizations } from '@/react/hooks/useUser';
import { WebEditorForm } from '@@/WebEditorForm';
import { TextTip } from '@@/Tip/TextTip';
type StackFileContent = string;
type Props = {
values: StackFileContent;
onChange: (values: StackFileContent) => void;
isComposeFormat?: boolean;
};
export function EditYamlFormSection({
values,
onChange,
isComposeFormat,
}: Props) {
// check if the user is allowed to edit the yaml
const environmentId = useEnvironmentId();
const { data: deploymentOptions } =
useEnvironmentDeploymentOptions(environmentId);
const roleHasAuth = useAuthorizations('K8sYAMLW');
const isAllowedToEdit = roleHasAuth && !deploymentOptions?.hideWebEditor;
const formId = 'kubernetes-deploy-editor';
return (
<div>
<WebEditorForm
value={values}
readonly={!isAllowedToEdit}
titleContent={<TitleContent isComposeFormat={isComposeFormat} />}
onChange={(values) => onChange(values)}
id={formId}
placeholder="Define or paste the content of your manifest file here"
yaml
/>
</div>
);
}
function TitleContent({ isComposeFormat }: { isComposeFormat?: boolean }) {
return (
<>
{isComposeFormat && (
<TextTip color="orange">
<p>
Portainer no longer supports{' '}
<a
href="https://docs.docker.com/compose/compose-file/"
target="_blank"
rel="noreferrer"
>
docker-compose
</a>{' '}
format manifests for Kubernetes deployments, and we have removed the{' '}
<a href="https://kompose.io/" target="_blank" rel="noreferrer">
Kompose
</a>{' '}
conversion tool which enables this. The reason for this is because
Kompose now poses a security risk, since it has a number of Common
Vulnerabilities and Exposures (CVEs).
</p>
<p>
Unfortunately, while the Kompose project has a maintainer and is
part of the CNCF, it is not being actively maintained. Releases are
very infrequent and new pull requests to the project (including ones
we&apos;ve submitted) are taking months to be merged, with new CVEs
arising in the meantime.
</p>
<p>
We advise installing your own instance of Kompose in a sandbox
environment, performing conversions of your Docker Compose files to
Kubernetes manifests and using those manifests to set up
applications.
</p>
</TextTip>
)}
{!isComposeFormat && (
<TextTip color="blue">
<p>
This feature allows you to deploy any kind of Kubernetes resource in
this environment (Deployment, Secret, ConfigMap...).
</p>
<p>
You can get more information about Kubernetes file format in the{' '}
<a
href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/"
target="_blank"
rel="noreferrer"
>
official documentation
</a>
.
</p>
</TextTip>
)}
</>
);
}

View file

@ -0,0 +1,38 @@
import { FormikErrors } from 'formik';
import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
type Props = {
onChange: (value: string) => void;
values: string;
errors: FormikErrors<string>;
isEdit: boolean;
};
export function NameFormSection({
onChange,
values: appName,
errors,
isEdit,
}: Props) {
return (
<FormControl
label="Name"
inputId="application_name"
errors={errors}
required
>
<Input
type="text"
value={appName ?? ''}
onChange={(e) => onChange(e.target.value)}
autoFocus
placeholder="e.g. my-app"
disabled={isEdit}
id="application_name"
data-cy="k8sAppCreate-applicationName"
/>
</FormControl>
);
}

View file

@ -0,0 +1,2 @@
export { NameFormSection } from './NameFormSection';
export { appNameValidation } from './nameValidation';

View file

@ -0,0 +1,43 @@
import { SchemaOf, string as yupString } from 'yup';
type ValidationData = {
existingNames: string[];
isEdit: boolean;
originalName?: string;
};
export function appNameValidation(
validationData?: ValidationData
): SchemaOf<string> {
return yupString()
.required('This field is required.')
.test(
'is-unique',
'An application with the same name already exists inside the selected namespace.',
(appName) => {
if (!validationData || !appName) {
return true;
}
// if creating, check if the name is unique
if (!validationData.isEdit) {
return !validationData.existingNames.includes(appName);
}
// if editing, the original name will be in the list of existing names
// remove it before checking if the name is unique
const updatedExistingNames = validationData.existingNames.filter(
(name) => name !== validationData.originalName
);
return !updatedExistingNames.includes(appName);
}
)
.test(
'is-valid',
"This field must consist of lower case alphanumeric characters or '-', contain at most 63 characters, start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').",
(appName) => {
if (!appName) {
return true;
}
return /^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/g.test(appName);
}
);
}

View file

@ -0,0 +1,53 @@
import { FormikErrors } from 'formik';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
import { FormControl } from '@@/form-components/FormControl';
import { PortainerSelect } from '@@/form-components/PortainerSelect';
type Props = {
onChange: (value: string) => void;
values: string;
errors: FormikErrors<string>;
isEdit: boolean;
};
export function NamespaceSelector({
values: value,
onChange,
errors,
isEdit,
}: Props) {
const environmentId = useEnvironmentId();
const { data: namespaces, ...namespacesQuery } =
useNamespacesQuery(environmentId);
const namespaceNames = Object.entries(namespaces ?? {})
.filter(([, ns]) => !ns.IsSystem)
.map(([nsName]) => ({
label: nsName,
value: nsName,
}));
return (
<FormControl
label="Namespace"
inputId="namespace-selector"
isLoading={namespacesQuery.isLoading}
errors={errors}
>
{namespaceNames.length > 0 && (
<PortainerSelect
value={value}
options={namespaceNames}
onChange={onChange}
disabled={isEdit}
noOptionsMessage={() => 'No namespaces found'}
placeholder="No namespaces found" // will only show when there are no options
inputId="namespace-selector"
data-cy="k8sAppCreate-nsSelect"
/>
)}
</FormControl>
);
}

View file

@ -0,0 +1,2 @@
export { NamespaceSelector } from './NamespaceSelector';
export { namespaceSelectorValidation } from './namespaceSelectorValidation';

View file

@ -0,0 +1,38 @@
import { SchemaOf, string } from 'yup';
type ValidationData = {
hasQuota: boolean;
isResourceQuotaCapacityExceeded: boolean;
namespaceOptionCount: number;
isAdmin: boolean;
};
const emptyValue =
'You do not have access to any namespace. Contact your administrator to get access to a namespace.';
export function namespaceSelectorValidation(
validationData?: ValidationData
): SchemaOf<string> {
const {
hasQuota,
isResourceQuotaCapacityExceeded,
namespaceOptionCount,
isAdmin,
} = validationData || {};
return string()
.required(emptyValue)
.typeError(emptyValue)
.test(
'resourceQuotaCapacityExceeded',
`This namespace has exhausted its resource capacity and you will not be able to deploy the application.${
isAdmin
? ''
: ' Contact your administrator to expand the capacity of the namespace.'
}`,
() => {
const hasQuotaExceeded = hasQuota && isResourceQuotaCapacityExceeded;
return !hasQuotaExceeded;
}
)
.test('namespaceOptionCount', emptyValue, () => !!namespaceOptionCount);
}