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

feat(edge/stacks): sync EE codechanges [EE-498] (#8580)

This commit is contained in:
Chaim Lev-Ari 2023-05-31 01:33:22 +07:00 committed by GitHub
parent 0ec7dfce69
commit 93bf630105
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 1572 additions and 424 deletions

View file

@ -0,0 +1,79 @@
import { useFormikContext } from 'formik';
import { TextTip } from '@@/Tip/TextTip';
import { WebEditorForm } from '@@/WebEditorForm';
import { DeploymentType } from '../../types';
import { FormValues } from './types';
export function ComposeForm({
handleContentChange,
hasKubeEndpoint,
}: {
hasKubeEndpoint: boolean;
handleContentChange: (type: DeploymentType, content: string) => void;
}) {
const { errors, values } = useFormikContext<FormValues>();
return (
<>
{hasKubeEndpoint && (
<TextTip>
<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>
)}
<WebEditorForm
value={values.content}
yaml
id="compose-editor"
placeholder="Define or paste the content of your docker compose file here"
onChange={(value) => handleContentChange(DeploymentType.Compose, value)}
error={errors.content}
readonly={hasKubeEndpoint}
>
<div>
You can get more information about Compose file format in the{' '}
<a
href="https://docs.docker.com/compose/compose-file/"
target="_blank"
rel="noreferrer"
>
official documentation
</a>
.
</div>
</WebEditorForm>
</>
);
}

View file

@ -0,0 +1,267 @@
import { Form, Formik, useFormikContext } from 'formik';
import { useState } from 'react';
import { array, boolean, number, object, SchemaOf, string } from 'yup';
import { EdgeGroupsSelector } from '@/react/edge/edge-stacks/components/EdgeGroupsSelector';
import { EdgeStackDeploymentTypeSelector } from '@/react/edge/edge-stacks/components/EdgeStackDeploymentTypeSelector';
import { DeploymentType, EdgeStack } from '@/react/edge/edge-stacks/types';
import { EnvironmentType } from '@/react/portainer/environments/types';
import { WebhookSettings } from '@/react/portainer/gitops/AutoUpdateFieldset/WebhookSettings';
import { baseEdgeStackWebhookUrl } from '@/portainer/helpers/webhookHelper';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { FormSection } from '@@/form-components/FormSection';
import { TextTip } from '@@/Tip/TextTip';
import { SwitchField } from '@@/form-components/SwitchField';
import { LoadingButton } from '@@/buttons';
import { FormError } from '@@/form-components/FormError';
import { PrivateRegistryFieldsetWrapper } from './PrivateRegistryFieldsetWrapper';
import { FormValues } from './types';
import { ComposeForm } from './ComposeForm';
import { KubernetesForm } from './KubernetesForm';
import { NomadForm } from './NomadForm';
import { GitForm } from './GitForm';
import { useValidateEnvironmentTypes } from './useEdgeGroupHasType';
import { atLeastTwo } from './atLeastTwo';
interface Props {
edgeStack: EdgeStack;
isSubmitting: boolean;
onSubmit: (values: FormValues) => void;
onEditorChange: (content: string) => void;
fileContent: string;
allowKubeToSelectCompose: boolean;
}
const forms = {
[DeploymentType.Compose]: ComposeForm,
[DeploymentType.Kubernetes]: KubernetesForm,
[DeploymentType.Nomad]: NomadForm,
};
export function EditEdgeStackForm({
isSubmitting,
edgeStack,
onSubmit,
onEditorChange,
fileContent,
allowKubeToSelectCompose,
}: Props) {
if (edgeStack.GitConfig) {
return <GitForm stack={edgeStack} />;
}
const formValues: FormValues = {
edgeGroups: edgeStack.EdgeGroups,
deploymentType: edgeStack.DeploymentType,
privateRegistryId: edgeStack.Registries?.[0],
content: fileContent,
useManifestNamespaces: edgeStack.UseManifestNamespaces,
prePullImage: edgeStack.PrePullImage,
retryDeploy: edgeStack.RetryDeploy,
webhookEnabled: !!edgeStack.Webhook,
};
return (
<Formik
initialValues={formValues}
onSubmit={onSubmit}
validationSchema={formValidation()}
>
<InnerForm
edgeStack={edgeStack}
isSubmitting={isSubmitting}
onEditorChange={onEditorChange}
allowKubeToSelectCompose={allowKubeToSelectCompose}
/>
</Formik>
);
}
function InnerForm({
onEditorChange,
edgeStack,
isSubmitting,
allowKubeToSelectCompose,
}: {
edgeStack: EdgeStack;
isSubmitting: boolean;
onEditorChange: (content: string) => void;
allowKubeToSelectCompose: boolean;
}) {
const {
values,
setFieldValue,
isValid,
errors,
setFieldError,
} = useFormikContext<FormValues>();
const { getCachedContent, setContentCache } = useCachedContent();
const { hasType } = useValidateEnvironmentTypes(values.edgeGroups);
const hasKubeEndpoint = hasType(EnvironmentType.EdgeAgentOnKubernetes);
const hasDockerEndpoint = hasType(EnvironmentType.EdgeAgentOnDocker);
const hasNomadEndpoint = hasType(EnvironmentType.EdgeAgentOnNomad);
const DeploymentForm = forms[values.deploymentType];
return (
<Form className="form-horizontal">
<EdgeGroupsSelector
value={values.edgeGroups}
onChange={(value) => setFieldValue('edgeGroups', value)}
error={errors.edgeGroups}
/>
{atLeastTwo(hasKubeEndpoint, hasDockerEndpoint, hasNomadEndpoint) && (
<TextTip>
There are no available deployment types when there is more than one
type of environment in your edge group selection (e.g. Kubernetes and
Docker environments). Please select edge groups that have environments
of the same type.
</TextTip>
)}
{values.deploymentType === DeploymentType.Compose && hasKubeEndpoint && (
<FormError>
Edge groups with kubernetes environments no longer support compose
deployment types in Portainer. Please select edge groups that only
have docker environments when using compose deployment types.
</FormError>
)}
<EdgeStackDeploymentTypeSelector
allowKubeToSelectCompose={allowKubeToSelectCompose}
value={values.deploymentType}
hasDockerEndpoint={hasType(EnvironmentType.EdgeAgentOnDocker)}
hasKubeEndpoint={hasType(EnvironmentType.EdgeAgentOnKubernetes)}
hasNomadEndpoint={hasType(EnvironmentType.EdgeAgentOnNomad)}
onChange={(value) => {
setFieldValue('content', getCachedContent(value));
setFieldValue('deploymentType', value);
}}
/>
<DeploymentForm
hasKubeEndpoint={hasType(EnvironmentType.EdgeAgentOnKubernetes)}
handleContentChange={handleContentChange}
/>
{isBE && (
<>
<FormSection title="Webhooks">
<div className="form-group">
<div className="col-sm-12">
<SwitchField
label="Create an Edge stack webhook"
checked={values.webhookEnabled}
labelClass="col-sm-3 col-lg-2"
onChange={(value) => setFieldValue('webhookEnabled', value)}
tooltip="Create a webhook (or callback URI) to automate the update of this stack. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and re-deploy this stack."
/>
</div>
</div>
{edgeStack.Webhook && (
<WebhookSettings
baseUrl={baseEdgeStackWebhookUrl()}
value={edgeStack.Webhook}
docsLink="todo"
/>
)}
</FormSection>
<PrivateRegistryFieldsetWrapper
value={values.privateRegistryId}
onChange={(value) => setFieldValue('privateRegistryId', value)}
isValid={isValid}
values={values}
stackName={edgeStack.Name}
onFieldError={(error) => setFieldError('privateRegistryId', error)}
error={errors.privateRegistryId}
/>
{values.deploymentType === DeploymentType.Compose && (
<>
<SwitchField
checked={values.prePullImage}
name="prePullImage"
label="Pre-pull images"
tooltip="When enabled, redeployment will be executed when image(s) is pulled successfully"
label-Class="col-sm-3 col-lg-2"
onChange={(value) => setFieldValue('prePullImage', value)}
/>
<SwitchField
checked={values.retryDeploy}
name="retryDeploy"
label="Retry deployment"
tooltip="When enabled, this will allow edge agent keep retrying deployment if failure occur"
label-Class="col-sm-3 col-lg-2"
onChange={(value) => setFieldValue('retryDeploy', value)}
/>
</>
)}
</>
)}
<FormSection title="Actions">
<div className="form-group">
<div className="col-sm-12">
<LoadingButton
className="!ml-0"
size="small"
disabled={!isValid}
isLoading={isSubmitting}
button-spinner="$ctrl.actionInProgress"
loadingText="Update in progress..."
>
Update the stack
</LoadingButton>
</div>
</div>
</FormSection>
</Form>
);
function handleContentChange(type: DeploymentType, content: string) {
setFieldValue('content', content);
setContentCache(type, content);
onEditorChange(content);
}
}
function useCachedContent() {
const [cachedContent, setCachedContent] = useState({
[DeploymentType.Compose]: '',
[DeploymentType.Kubernetes]: '',
[DeploymentType.Nomad]: '',
});
function handleChangeContent(type: DeploymentType, content: string) {
setCachedContent((cache) => ({ ...cache, [type]: content }));
}
return {
setContentCache: handleChangeContent,
getCachedContent: (type: DeploymentType) => cachedContent[type],
};
}
function formValidation(): SchemaOf<FormValues> {
return object({
content: string().required('Content is required'),
deploymentType: number()
.oneOf([0, 1, 2])
.required('Deployment type is required'),
privateRegistryId: number().optional(),
prePullImage: boolean().default(false),
retryDeploy: boolean().default(false),
useManifestNamespaces: boolean().default(false),
edgeGroups: array()
.of(number().required())
.required()
.min(1, 'At least one edge group is required'),
webhookEnabled: boolean().default(false),
});
}

View file

@ -0,0 +1,279 @@
import { Form, Formik, useFormikContext } from 'formik';
import { useRouter } from '@uirouter/react';
import { AuthFieldset } from '@/react/portainer/gitops/AuthFieldset';
import { AutoUpdateFieldset } from '@/react/portainer/gitops/AutoUpdateFieldset';
import {
parseAutoUpdateResponse,
transformAutoUpdateViewModel,
} from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
import { InfoPanel } from '@/react/portainer/gitops/InfoPanel';
import { RefField } from '@/react/portainer/gitops/RefField';
import { AutoUpdateModel, GitAuthModel } from '@/react/portainer/gitops/types';
import {
baseEdgeStackWebhookUrl,
createWebhookId,
} from '@/portainer/helpers/webhookHelper';
import {
parseAuthResponse,
transformGitAuthenticationViewModel,
} from '@/react/portainer/gitops/AuthFieldset/utils';
import { EdgeGroup } from '@/react/edge/edge-groups/types';
import { DeploymentType, EdgeStack } from '@/react/edge/edge-stacks/types';
import { EdgeGroupsSelector } from '@/react/edge/edge-stacks/components/EdgeGroupsSelector';
import { EdgeStackDeploymentTypeSelector } from '@/react/edge/edge-stacks/components/EdgeStackDeploymentTypeSelector';
import { useCurrentUser } from '@/react/hooks/useUser';
import { useCreateGitCredentialMutation } from '@/react/portainer/account/git-credentials/git-credentials.service';
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
import { EnvironmentType } from '@/react/portainer/environments/types';
import { LoadingButton } from '@@/buttons';
import { FormSection } from '@@/form-components/FormSection';
import { TextTip } from '@@/Tip/TextTip';
import { FormError } from '@@/form-components/FormError';
import { useValidateEnvironmentTypes } from '../useEdgeGroupHasType';
import { atLeastTwo } from '../atLeastTwo';
import { useUpdateEdgeStackGitMutation } from './useUpdateEdgeStackGitMutation';
interface FormValues {
groupIds: EdgeGroup['Id'][];
deploymentType: DeploymentType;
autoUpdate: AutoUpdateModel;
refName: string;
authentication: GitAuthModel;
}
export function GitForm({ stack }: { stack: EdgeStack }) {
const router = useRouter();
const updateStackMutation = useUpdateEdgeStackGitMutation();
const saveCredentialsMutation = useCreateGitCredentialMutation();
const { user } = useCurrentUser();
if (!stack.GitConfig) {
return null;
}
const gitConfig = stack.GitConfig;
const initialValues: FormValues = {
groupIds: stack.EdgeGroups,
deploymentType: stack.DeploymentType,
autoUpdate: parseAutoUpdateResponse(stack.AutoUpdate),
refName: stack.GitConfig.ReferenceName,
authentication: parseAuthResponse(stack.GitConfig.Authentication),
};
const webhookId = stack.AutoUpdate?.Webhook || createWebhookId();
return (
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
{({ values, isValid }) => {
return (
<InnerForm
webhookId={webhookId}
onUpdateSettingsClick={handleUpdateSettings}
gitPath={gitConfig.ConfigFilePath}
gitUrl={gitConfig.URL}
isLoading={updateStackMutation.isLoading}
isUpdateVersion={!!updateStackMutation.variables?.updateVersion}
/>
);
async function handleUpdateSettings() {
if (!isValid) {
return;
}
const credentialId = await saveCredentialsIfRequired(
values.authentication
);
updateStackMutation.mutate(getPayload(values, credentialId, false), {
onSuccess() {
notifySuccess('Success', 'Stack updated successfully');
router.stateService.reload();
},
});
}
}}
</Formik>
);
async function handleSubmit(values: FormValues) {
const credentialId = await saveCredentialsIfRequired(values.authentication);
updateStackMutation.mutate(getPayload(values, credentialId, true), {
onSuccess() {
notifySuccess('Success', 'Stack updated successfully');
router.stateService.reload();
},
});
}
function getPayload(
{ authentication, autoUpdate, ...values }: FormValues,
credentialId: number | undefined,
updateVersion: boolean
) {
return {
updateVersion,
id: stack.Id,
authentication: transformGitAuthenticationViewModel({
...authentication,
RepositoryGitCredentialID: credentialId,
}),
autoUpdate: transformAutoUpdateViewModel(autoUpdate, webhookId),
...values,
};
}
async function saveCredentialsIfRequired(authentication: GitAuthModel) {
if (
!authentication.SaveCredential ||
!authentication.RepositoryPassword ||
!authentication.NewCredentialName
) {
return authentication.RepositoryGitCredentialID;
}
try {
const credential = await saveCredentialsMutation.mutateAsync({
userId: user.Id,
username: authentication.RepositoryUsername,
password: authentication.RepositoryPassword,
name: authentication.NewCredentialName,
});
return credential.id;
} catch (err) {
notifyError('Error', err as Error, 'Unable to save credentials');
return undefined;
}
}
}
function InnerForm({
gitUrl,
gitPath,
isLoading,
isUpdateVersion,
onUpdateSettingsClick,
webhookId,
}: {
gitUrl: string;
gitPath: string;
isLoading: boolean;
isUpdateVersion: boolean;
onUpdateSettingsClick(): void;
webhookId: string;
}) {
const { values, setFieldValue, isValid, handleSubmit, errors, dirty } =
useFormikContext<FormValues>();
const { hasType } = useValidateEnvironmentTypes(values.groupIds);
const hasKubeEndpoint = hasType(EnvironmentType.EdgeAgentOnKubernetes);
const hasDockerEndpoint = hasType(EnvironmentType.EdgeAgentOnDocker);
const hasNomadEndpoint = hasType(EnvironmentType.EdgeAgentOnNomad);
return (
<Form className="form-horizontal" onSubmit={handleSubmit}>
<EdgeGroupsSelector
value={values.groupIds}
onChange={(value) => setFieldValue('groupIds', value)}
error={errors.groupIds}
/>
{atLeastTwo(hasKubeEndpoint, hasDockerEndpoint, hasNomadEndpoint) && (
<TextTip>
There are no available deployment types when there is more than one
type of environment in your edge group selection (e.g. Kubernetes and
Docker environments). Please select edge groups that have environments
of the same type.
</TextTip>
)}
{values.deploymentType === DeploymentType.Compose && hasKubeEndpoint && (
<FormError>
Edge groups with kubernetes environments no longer support compose
deployment types in Portainer. Please select edge groups that only
have docker environments when using compose deployment types.
</FormError>
)}
<EdgeStackDeploymentTypeSelector
value={values.deploymentType}
hasDockerEndpoint={hasType(EnvironmentType.EdgeAgentOnDocker)}
hasKubeEndpoint={hasType(EnvironmentType.EdgeAgentOnKubernetes)}
hasNomadEndpoint={hasType(EnvironmentType.EdgeAgentOnNomad)}
onChange={(value) => {
setFieldValue('deploymentType', value);
}}
/>
<FormSection title="Update from git repository">
<InfoPanel
className="text-muted small"
url={gitUrl}
type="Edge stack"
configFilePath={gitPath}
/>
<AutoUpdateFieldset
webhookId={webhookId}
value={values.autoUpdate}
onChange={(value) =>
setFieldValue('autoUpdate', {
...values.autoUpdate,
...value,
})
}
baseWebhookUrl={baseEdgeStackWebhookUrl()}
errors={errors.autoUpdate}
/>
</FormSection>
<FormSection title="Advanced configuration" isFoldable>
<RefField
value={values.refName}
onChange={(value) => setFieldValue('refName', value)}
model={{ ...values.authentication, RepositoryURL: gitUrl }}
error={errors.refName}
isUrlValid
/>
<AuthFieldset
value={values.authentication}
isAuthExplanationVisible
onChange={(value) =>
Object.entries(value).forEach(([key, value]) => {
setFieldValue(`authentication.${key}`, value);
})
}
errors={errors.authentication}
/>
</FormSection>
<FormSection title="Actions">
<LoadingButton
disabled={!dirty || !isValid || isLoading}
isLoading={isUpdateVersion && isLoading}
loadingText="updating stack..."
>
Pull and update stack
</LoadingButton>
<LoadingButton
type="button"
disabled={!dirty || !isValid || isLoading}
isLoading={!isUpdateVersion && isLoading}
loadingText="updating settings..."
onClick={onUpdateSettingsClick}
>
Update settings
</LoadingButton>
</FormSection>
</Form>
);
}

View file

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

View file

@ -0,0 +1,39 @@
import { useMutation } from 'react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { mutationOptions, withError } from '@/react-tools/react-query';
import {
AutoUpdateResponse,
GitAuthenticationResponse,
} from '@/react/portainer/gitops/types';
import { buildUrl } from '@/react/edge/edge-stacks/queries/buildUrl';
import { DeploymentType, EdgeStack } from '@/react/edge/edge-stacks/types';
import { EdgeGroup } from '@/react/edge/edge-groups/types';
interface UpdateEdgeStackGitPayload {
id: EdgeStack['Id'];
autoUpdate: AutoUpdateResponse | null;
refName: string;
authentication: GitAuthenticationResponse | null;
groupIds: EdgeGroup['Id'][];
deploymentType: DeploymentType;
updateVersion: boolean;
}
export function useUpdateEdgeStackGitMutation() {
return useMutation(
updateEdgeStackGit,
mutationOptions(withError('Failed updating stack'))
);
}
async function updateEdgeStackGit({
id,
...payload
}: UpdateEdgeStackGitPayload) {
try {
await axios.put(buildUrl(id, 'git'), payload);
} catch (err) {
throw parseAxiosError(err as Error, 'Failed updating stack');
}
}

View file

@ -0,0 +1,54 @@
import { useFormikContext } from 'formik';
import { SwitchField } from '@@/form-components/SwitchField';
import { WebEditorForm } from '@@/WebEditorForm';
import { DeploymentType } from '../../types';
import { FormValues } from './types';
export function KubernetesForm({
handleContentChange,
}: {
handleContentChange: (type: DeploymentType, content: string) => void;
}) {
const { errors, values, setFieldValue } = useFormikContext<FormValues>();
return (
<>
<div className="form-group">
<div className="col-sm-12">
<SwitchField
label="Use namespace(s) specified from manifest"
tooltip="If you have defined namespaces in your deployment file turning this on will enforce the use of those only in the deployment"
checked={values.useManifestNamespaces}
onChange={(value) => setFieldValue('useManifestNamespaces', value)}
/>
</div>
</div>
<WebEditorForm
value={values.content}
yaml
id="kube-manifest-editor"
placeholder="Define or paste the content of your manifest here"
onChange={(value) =>
handleContentChange(DeploymentType.Kubernetes, value)
}
error={errors.content}
>
<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>
</WebEditorForm>
</>
);
}

View file

@ -0,0 +1,38 @@
import { useFormikContext } from 'formik';
import { WebEditorForm } from '@@/WebEditorForm';
import { DeploymentType } from '../../types';
import { FormValues } from './types';
export function NomadForm({
handleContentChange,
}: {
handleContentChange: (type: DeploymentType, content: string) => void;
}) {
const { errors, values } = useFormikContext<FormValues>();
return (
<WebEditorForm
value={values.content}
yaml
id="kube-manifest-editor"
placeholder="Define or paste the content of your manifest here"
onChange={(value) => handleContentChange(DeploymentType.Nomad, value)}
error={errors.content}
>
<p>
You can get more information about Nomad file format in the{' '}
<a
href="https://www.nomadproject.io/docs/job-specification"
target="_blank"
rel="noreferrer"
>
official documentation
</a>
.
</p>
</WebEditorForm>
);
}

View file

@ -0,0 +1,80 @@
import _ from 'lodash';
import { notifyError } from '@/portainer/services/notifications';
import { PrivateRegistryFieldset } from '@/react/edge/edge-stacks/components/PrivateRegistryFieldset';
import { useCreateStackFromFileContent } from '@/react/edge/edge-stacks/queries/useCreateStackFromFileContent';
import { useRegistries } from '@/react/portainer/registries/queries/useRegistries';
import { FormValues } from './types';
export function PrivateRegistryFieldsetWrapper({
value,
isValid,
error,
onChange,
values,
stackName,
onFieldError,
}: {
value: FormValues['privateRegistryId'];
isValid: boolean;
error?: string;
onChange: (value?: number) => void;
values: FormValues;
stackName: string;
onFieldError: (message: string) => void;
}) {
const dryRunMutation = useCreateStackFromFileContent();
const registriesQuery = useRegistries();
if (!registriesQuery.data) {
return null;
}
return (
<PrivateRegistryFieldset
value={value}
formInvalid={!isValid}
errorMessage={error}
registries={registriesQuery.data}
onChange={() => matchRegistry()}
onSelect={(value) => onChange(value)}
isActive={!!value}
clearRegistries={() => onChange(undefined)}
/>
);
async function matchRegistry() {
try {
const response = await dryRunMutation.mutateAsync({
name: `${stackName}-dryrun`,
stackFileContent: values.content,
edgeGroups: values.edgeGroups,
deploymentType: values.deploymentType,
dryRun: true,
});
if (response.Registries.length === 0) {
onChange(undefined);
return;
}
const validRegistry = onlyOne(response.Registries);
if (validRegistry) {
onChange(response.Registries[0]);
} else {
onChange(undefined);
onFieldError(
'Images need to be from a single registry, please edit and reload'
);
}
} catch (err) {
notifyError('Failure', err as Error, 'Unable to retrieve registries');
}
}
function onlyOne<T extends string | number>(arr: Array<T>) {
return _.uniq(arr).length === 1;
}
}

View file

@ -0,0 +1,3 @@
export function atLeastTwo(a: boolean, b: boolean, c: boolean) {
return (a && b) || (a && c) || (b && c);
}

View file

@ -0,0 +1,13 @@
import { EdgeGroup } from '@/react/edge/edge-groups/types';
import { DeploymentType } from '@/react/edge/edge-stacks/types';
export interface FormValues {
edgeGroups: EdgeGroup['Id'][];
deploymentType: DeploymentType;
privateRegistryId?: number;
content: string;
useManifestNamespaces: boolean;
prePullImage: boolean;
retryDeploy: boolean;
webhookEnabled: boolean;
}

View file

@ -0,0 +1,26 @@
import _ from 'lodash';
import { useCallback } from 'react';
import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups';
import { EdgeGroup } from '@/react/edge/edge-groups/types';
import { EnvironmentType } from '@/react/portainer/environments/types';
export function useValidateEnvironmentTypes(groupIds: Array<EdgeGroup['Id']>) {
const edgeGroupsQuery = useEdgeGroups();
const edgeGroups = edgeGroupsQuery.data || [];
const modelEdgeGroups = _.compact(
groupIds.map((id) => edgeGroups.find((e) => e.Id === id))
);
const endpointTypes = modelEdgeGroups.flatMap((group) => group.EndpointTypes);
const hasType = useCallback(
(type: EnvironmentType) => endpointTypes.includes(type),
[endpointTypes]
);
return {
hasType,
};
}