) => onChange && onChange(value),
+ [onChange]
+ );
+
+ const { errors } = useValidation(value);
+
+ return (
+ <>
+
+
+
+ 'Gitops Edge Configuration' requires relative path volumes
+ to be enabled first, as it uses this feature as the base mechanism.
+
+
+
+
+
+
+ innerOnChange({ SupportRelativePath: value })}
+ />
+
+
+
+ {value.SupportRelativePath && (
+ <>
+
+
+
+ For relative path volumes use with Docker Swarm, you must have a
+ network filesystem which all of your nodes can access.
+
+
+
+
+
+
+
+
+ innerOnChange({ FilesystemPath: e.target.value })
+ }
+ />
+
+
+
+
+
+
+
+ When enabled, corresponding Edge ID will be passed through as an
+ environment variable: PORTAINER_EDGE_ID.
+
+
+
+
+
+
+
+ innerOnChange({ SupportPerDeviceConfigs: value })
+ }
+ />
+
+
+
+ {value.SupportPerDeviceConfigs && (
+ <>
+
+
+
+ Specify the directory name where your configuration will be
+ located. This will allow you to manage device configuration
+ settings with a Git repo as your template.
+
+
+
+
+
+
+
+
+ innerOnChange({ PerDeviceConfigsPath: value })
+ }
+ placeholder="config"
+ model={gitModel || dummyGitForm}
+ readOnly={readonly}
+ dirOnly
+ />
+
+
+
+
+
+
+
+ Select which rule to use when matching configuration with
+ Portainer Edge ID either on a per-device basis or group-wide
+ with an Edge Group. Only configurations that match the
+ selected rule will be accessible through their corresponding
+ paths. Deployments that rely on accessing the configuration
+ may experience errors.
+
+
+
+
+
+
+
+
+ innerOnChange({
+ PerDeviceConfigsMatchType: e.target.value,
+ })
+ }
+ options={[
+ {
+ label: '',
+ value: '',
+ },
+ {
+ label: 'Match file name with Portainer Edge ID',
+ value: 'file',
+ },
+ {
+ label: 'Match folder name with Portainer Edge ID',
+ value: 'dir',
+ },
+ ]}
+ disabled={readonly}
+ />
+
+
+
+
+
+
+
+
+ innerOnChange({
+ PerDeviceConfigsGroupMatchType: e.target.value,
+ })
+ }
+ options={[
+ {
+ label: '',
+ value: '',
+ },
+ {
+ label: 'Match file name with Edge Group',
+ value: 'file',
+ },
+ {
+ label: 'Match folder name with Edge Group',
+ value: 'dir',
+ },
+ ]}
+ disabled={readonly}
+ />
+
+
+
+
+
+
+
+
+ You can use it as an environment variable with an image:{' '}
+
myapp:${PORTAINER_EDGE_ID}
or{' '}
+
myapp:${PORTAINER_EDGE_GROUP}
. You
+ can also use it with the relative path for volumes:{' '}
+
+ ./config/${PORTAINER_EDGE_ID}:/myapp/config
+
{' '}
+ or{' '}
+
+ ./config/${PORTAINER_EDGE_GROUP}:/myapp/groupconfig
+
+ . More documentation can be found{' '}
+
+ here
+
+ .
+
+
+
+
+ >
+ )}
+ >
+ )}
+ >
+ );
+}
diff --git a/app/react/portainer/gitops/RelativePathFieldset/useValidation.ts b/app/react/portainer/gitops/RelativePathFieldset/useValidation.ts
new file mode 100644
index 000000000..01d61c0a7
--- /dev/null
+++ b/app/react/portainer/gitops/RelativePathFieldset/useValidation.ts
@@ -0,0 +1,27 @@
+import { useEffect, useState } from 'react';
+import { FormikErrors, yupToFormErrors } from 'formik';
+
+import { RelativePathModel } from '@/react/portainer/gitops/types';
+import { relativePathValidation } from '@/react/portainer/gitops/RelativePathFieldset/validation';
+
+export function useValidation(value: RelativePathModel) {
+ const [errors, setErrors] = useState>({});
+
+ useEffect(() => {
+ async function valide() {
+ try {
+ await relativePathValidation().validate(value, {
+ strict: true,
+ abortEarly: false,
+ });
+ setErrors({});
+ } catch (error) {
+ setErrors(yupToFormErrors(error));
+ }
+ }
+
+ valide();
+ }, [value]);
+
+ return { errors };
+}
diff --git a/app/react/portainer/gitops/RelativePathFieldset/utils.ts b/app/react/portainer/gitops/RelativePathFieldset/utils.ts
new file mode 100644
index 000000000..b13358b7e
--- /dev/null
+++ b/app/react/portainer/gitops/RelativePathFieldset/utils.ts
@@ -0,0 +1,28 @@
+import { EdgeStack } from '@/react/edge/edge-stacks/types';
+
+import { GitFormModel, RelativePathModel } from '../types';
+
+export function parseRelativePathResponse(stack: EdgeStack): RelativePathModel {
+ return {
+ SupportRelativePath: stack.SupportRelativePath,
+ FilesystemPath: stack.FilesystemPath,
+ SupportPerDeviceConfigs: stack.SupportPerDeviceConfigs,
+ PerDeviceConfigsMatchType: stack.PerDeviceConfigsMatchType,
+ PerDeviceConfigsGroupMatchType: stack.PerDeviceConfigsGroupMatchType,
+ PerDeviceConfigsPath: stack.PerDeviceConfigsPath,
+ };
+}
+
+export const dummyGitForm: GitFormModel = {
+ RepositoryURL: '',
+ RepositoryURLValid: false,
+ RepositoryAuthentication: false,
+ RepositoryUsername: '',
+ RepositoryPassword: '',
+ AdditionalFiles: [],
+ RepositoryReferenceName: '',
+ ComposeFilePathInRepository: '',
+ NewCredentialName: '',
+ SaveCredential: false,
+ TLSSkipVerify: false,
+};
diff --git a/app/react/portainer/gitops/RelativePathFieldset/validation.ts b/app/react/portainer/gitops/RelativePathFieldset/validation.ts
new file mode 100644
index 000000000..351e188bf
--- /dev/null
+++ b/app/react/portainer/gitops/RelativePathFieldset/validation.ts
@@ -0,0 +1,24 @@
+import { boolean, object, SchemaOf, string } from 'yup';
+
+import { RelativePathModel } from '@/react/portainer/gitops/types';
+
+export function relativePathValidation(): SchemaOf {
+ return object({
+ SupportRelativePath: boolean().default(false),
+ FilesystemPath: string()
+ .when(['SupportRelativePath'], {
+ is: true,
+ then: string().required('Local filesystem path is required'),
+ })
+ .default(''),
+ SupportPerDeviceConfigs: boolean().default(false),
+ PerDeviceConfigsPath: string()
+ .when(['SupportPerDeviceConfigs'], {
+ is: true,
+ then: string().required('Directory is required'),
+ })
+ .default(''),
+ PerDeviceConfigsMatchType: string().oneOf(['', 'file', 'dir']),
+ PerDeviceConfigsGroupMatchType: string().oneOf(['', 'file', 'dir']),
+ });
+}
diff --git a/app/react/portainer/gitops/index.ts b/app/react/portainer/gitops/index.ts
new file mode 100644
index 000000000..e99f7c752
--- /dev/null
+++ b/app/react/portainer/gitops/index.ts
@@ -0,0 +1,32 @@
+import angular from 'angular';
+
+import { r2a } from '@/react-tools/react2angular';
+import { withUIRouter } from '@/react-tools/withUIRouter';
+import { withReactQuery } from '@/react-tools/withReactQuery';
+import { PathSelector } from '@/react/portainer/gitops/ComposePathField/PathSelector';
+import { RelativePathFieldset } from '@/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset';
+
+export const ngModule = angular
+ .module('portainer.app.react.gitops', [])
+ .component(
+ 'pathSelector',
+ r2a(withUIRouter(withReactQuery(PathSelector)), [
+ 'value',
+ 'onChange',
+ 'placeholder',
+ 'model',
+ 'dirOnly',
+ 'readOnly',
+ ])
+ )
+ .component(
+ 'relativePathFieldset',
+ r2a(withUIRouter(withReactQuery(RelativePathFieldset)), [
+ 'value',
+ 'gitModel',
+ 'onChange',
+ 'readonly',
+ ])
+ );
+
+export const gitopsModule = ngModule.name;
diff --git a/app/react/portainer/gitops/types.ts b/app/react/portainer/gitops/types.ts
index 0501929ca..49f75af11 100644
--- a/app/react/portainer/gitops/types.ts
+++ b/app/react/portainer/gitops/types.ts
@@ -69,3 +69,12 @@ export interface GitFormModel extends GitAuthModel {
*/
AutoUpdate?: AutoUpdateModel;
}
+
+export interface RelativePathModel {
+ SupportRelativePath: boolean;
+ FilesystemPath?: string;
+ SupportPerDeviceConfigs?: boolean;
+ PerDeviceConfigsPath?: string;
+ PerDeviceConfigsMatchType?: string;
+ PerDeviceConfigsGroupMatchType?: string;
+}