diff --git a/app/portainer/components/form-components/FormControl/FormControl.stories.tsx b/app/portainer/components/form-components/FormControl/FormControl.stories.tsx
new file mode 100644
index 000000000..4f88e75ab
--- /dev/null
+++ b/app/portainer/components/form-components/FormControl/FormControl.stories.tsx
@@ -0,0 +1,54 @@
+import { Meta } from '@storybook/react';
+import { useState } from 'react';
+
+import { TextInput, Select } from '../Input';
+
+import { FormControl } from './FormControl';
+
+export default {
+ title: 'Components/Form/Control',
+} as Meta;
+
+interface TextFieldProps {
+ label: string;
+ tooltip?: string;
+}
+
+export function TextField({ label, tooltip = '' }: TextFieldProps) {
+ const [value, setValue] = useState('');
+ const inputId = 'input';
+ return (
+
+
+
+ );
+}
+
+TextField.args = {
+ label: 'label',
+ tooltip: '',
+};
+
+export function SelectField({ label, tooltip = '' }: TextFieldProps) {
+ const options = [
+ { value: 1, label: 'one' },
+ { value: 2, label: 'two' },
+ ];
+ const [value, setValue] = useState(0);
+ const inputId = 'input';
+ return (
+
+
+ );
+}
+
+SelectField.args = {
+ label: 'select',
+ tooltip: '',
+};
diff --git a/app/portainer/components/form-components/FormControl/FormControl.test.tsx b/app/portainer/components/form-components/FormControl/FormControl.test.tsx
new file mode 100644
index 000000000..225720ee2
--- /dev/null
+++ b/app/portainer/components/form-components/FormControl/FormControl.test.tsx
@@ -0,0 +1,30 @@
+import { render } from '@testing-library/react';
+
+import { FormControl, Props } from './FormControl';
+
+function renderDefault({
+ inputId = 'id',
+ label,
+ tooltip = '',
+
+ errors,
+}: Partial) {
+ return render(
+
+
+
+ );
+}
+
+test('should display a Input component', async () => {
+ const label = 'test label';
+ const { findByText } = renderDefault({ label });
+
+ const inputElem = await findByText(label);
+ expect(inputElem).toBeTruthy();
+});
diff --git a/app/portainer/components/form-components/FormControl/FormControl.tsx b/app/portainer/components/form-components/FormControl/FormControl.tsx
new file mode 100644
index 000000000..91f4126fa
--- /dev/null
+++ b/app/portainer/components/form-components/FormControl/FormControl.tsx
@@ -0,0 +1,41 @@
+import { PropsWithChildren, ReactNode } from 'react';
+
+import { Tooltip } from '@/portainer/components/Tooltip';
+
+export interface Props {
+ inputId: string;
+ label: string | ReactNode;
+ tooltip?: string;
+ children: ReactNode;
+ errors?: string | ReactNode;
+}
+
+export function FormControl({
+ inputId,
+ label,
+ tooltip = '',
+ children,
+ errors,
+}: PropsWithChildren) {
+ return (
+
+
+
+
+
{children}
+
+
+ {errors && (
+
+ )}
+
+ );
+}
diff --git a/app/portainer/components/form-components/FormControl/index.ts b/app/portainer/components/form-components/FormControl/index.ts
new file mode 100644
index 000000000..3d8eba866
--- /dev/null
+++ b/app/portainer/components/form-components/FormControl/index.ts
@@ -0,0 +1 @@
+export { FormControl } from './FormControl';
diff --git a/app/portainer/components/form-components/Input/BaseInput.tsx b/app/portainer/components/form-components/Input/BaseInput.tsx
new file mode 100644
index 000000000..20d342a41
--- /dev/null
+++ b/app/portainer/components/form-components/Input/BaseInput.tsx
@@ -0,0 +1,41 @@
+import clsx from 'clsx';
+import { HTMLInputTypeAttribute } from 'react';
+
+import { InputProps } from './types';
+
+interface Props extends InputProps {
+ type?: HTMLInputTypeAttribute;
+ onChange(value: string): void;
+ value: number | string;
+ component?: 'input' | 'textarea';
+ rows?: number;
+ readonly?: boolean;
+}
+
+export function BaseInput({
+ component = 'input',
+ value,
+ disabled,
+ id,
+ readonly,
+ required,
+ type,
+ className,
+ rows,
+ onChange,
+}: Props) {
+ const Component = component;
+ return (
+ onChange(e.target.value)}
+ rows={rows}
+ />
+ );
+}
diff --git a/app/portainer/components/form-components/Input/NumberInput.stories.tsx b/app/portainer/components/form-components/Input/NumberInput.stories.tsx
new file mode 100644
index 000000000..3bc1ee4b0
--- /dev/null
+++ b/app/portainer/components/form-components/Input/NumberInput.stories.tsx
@@ -0,0 +1,25 @@
+import { Meta, Story } from '@storybook/react';
+import { useState } from 'react';
+
+import { NumberInput } from './NumberInput';
+
+export default {
+ title: 'Components/Form/NumberInput',
+ args: {
+ disabled: false,
+ },
+} as Meta;
+
+interface Args {
+ disabled?: boolean;
+}
+
+export function Example({ disabled }: Args) {
+ const [value, setValue] = useState(0);
+ return ;
+}
+
+export const DisabledNumberInput: Story = Example.bind({});
+DisabledNumberInput.args = {
+ disabled: true,
+};
diff --git a/app/portainer/components/form-components/Input/NumberInput.tsx b/app/portainer/components/form-components/Input/NumberInput.tsx
new file mode 100644
index 000000000..a05d909cf
--- /dev/null
+++ b/app/portainer/components/form-components/Input/NumberInput.tsx
@@ -0,0 +1,33 @@
+import clsx from 'clsx';
+
+import { BaseInput } from './BaseInput';
+import { InputProps } from './types';
+
+interface Props extends InputProps {
+ value: number;
+ readonly?: boolean;
+ onChange(value: number): void;
+}
+
+export function NumberInput({
+ disabled,
+ required,
+ id,
+ value,
+ className,
+ readonly,
+ onChange,
+}: Props) {
+ return (
+ onChange(parseFloat(value))}
+ />
+ );
+}
diff --git a/app/portainer/components/form-components/Input/Select.stories.tsx b/app/portainer/components/form-components/Input/Select.stories.tsx
new file mode 100644
index 000000000..4199c9bd1
--- /dev/null
+++ b/app/portainer/components/form-components/Input/Select.stories.tsx
@@ -0,0 +1,36 @@
+import { Meta, Story } from '@storybook/react';
+import { useState } from 'react';
+
+import { Select } from './Select';
+
+export default {
+ title: 'Components/Form/Select',
+ args: {
+ disabled: false,
+ },
+} as Meta;
+
+interface Args {
+ disabled?: boolean;
+}
+
+export function Example({ disabled }: Args) {
+ const [value, setValue] = useState(0);
+ const options = [
+ { value: 1, label: 'one' },
+ { value: 2, label: 'two' },
+ ];
+ return (
+