diff --git a/app/portainer/components/form-components/Slider/Slider.module.css b/app/portainer/components/form-components/Slider/Slider.module.css
new file mode 100644
index 000000000..9eb1a62e8
--- /dev/null
+++ b/app/portainer/components/form-components/Slider/Slider.module.css
@@ -0,0 +1,32 @@
+.root {
+ margin: 0 25px;
+}
+
+.slider {
+ padding-top: 50px;
+}
+
+.slider :global .rc-slider-handle {
+ width: 32px;
+ height: 32px;
+ margin-top: -14px;
+ border-radius: 16px;
+ cursor: pointer;
+ background-color: #0db9f0;
+}
+
+.slider :global .rc-slider-handle:after {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ width: 8px;
+ height: 8px;
+ background: #ffffff;
+ border-radius: 4px;
+ content: '';
+}
+
+.slider :global .rc-slider-mark-text,
+.slider :global .rc-slider-tooltip-inner {
+ font-family: Montserrat, serif;
+}
diff --git a/app/portainer/components/form-components/Slider/Slider.stories.tsx b/app/portainer/components/form-components/Slider/Slider.stories.tsx
new file mode 100644
index 000000000..76f14457a
--- /dev/null
+++ b/app/portainer/components/form-components/Slider/Slider.stories.tsx
@@ -0,0 +1,35 @@
+import { Meta, Story } from '@storybook/react';
+import { useEffect, useState } from 'react';
+
+import { Slider, Props } from './Slider';
+
+export default {
+ component: Slider,
+ title: 'Components/Form/Slider',
+} as Meta;
+
+function Template({ value, min, max, step }: JSX.IntrinsicAttributes & Props) {
+ const [sliderValue, setSliderValue] = useState(min);
+
+ useEffect(() => {
+ setSliderValue(value);
+ }, [value]);
+
+ return (
+
+ );
+}
+
+export const Primary: Story = Template.bind({});
+Primary.args = {
+ min: 0,
+ max: 100,
+ step: 1,
+ value: 5,
+};
diff --git a/app/portainer/components/form-components/Slider/Slider.test.tsx b/app/portainer/components/form-components/Slider/Slider.test.tsx
new file mode 100644
index 000000000..403881753
--- /dev/null
+++ b/app/portainer/components/form-components/Slider/Slider.test.tsx
@@ -0,0 +1,22 @@
+import { render } from '@/react-tools/test-utils';
+
+import { Slider, Props } from './Slider';
+
+function renderDefault({
+ min = 0,
+ max = 10,
+ step = 1,
+ value = min,
+ onChange = () => {},
+}: Partial = {}) {
+ return render(
+
+ );
+}
+
+test('should display a Slider component', async () => {
+ const { getByRole } = renderDefault({});
+
+ const handle = getByRole('slider');
+ expect(handle).toBeTruthy();
+});
diff --git a/app/portainer/components/form-components/Slider/Slider.tsx b/app/portainer/components/form-components/Slider/Slider.tsx
new file mode 100644
index 000000000..e60c1fb57
--- /dev/null
+++ b/app/portainer/components/form-components/Slider/Slider.tsx
@@ -0,0 +1,42 @@
+import RcSlider from 'rc-slider';
+
+import styles from './Slider.module.css';
+import 'rc-slider/assets/index.css';
+
+export interface Props {
+ min: number;
+ max: number;
+ step: number;
+ value: number;
+ onChange: (value: number) => void;
+}
+
+export function Slider({ min, max, step, value, onChange }: Props) {
+ const SliderWithTooltip = RcSlider.createSliderWithTooltip(RcSlider);
+ const marks = {
+ [min]: translateMinValue(min),
+ [max]: max.toString(),
+ };
+
+ return (
+
+
+
+ );
+}
+
+function translateMinValue(value: number) {
+ if (value === 0) {
+ return 'unlimited';
+ }
+ return value.toString();
+}
diff --git a/package.json b/package.json
index e29230ed7..b31904c05 100644
--- a/package.json
+++ b/package.json
@@ -105,6 +105,7 @@
"moment": "^2.21.0",
"ng-file-upload": "~12.2.13",
"parse-duration": "^1.0.0",
+ "rc-slider": "^9.7.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-tooltip": "^4.2.21",
diff --git a/yarn.lock b/yarn.lock
index 45e92ff81..d8898b5a3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1637,13 +1637,34 @@
core-js-pure "^3.16.0"
regenerator-runtime "^0.13.4"
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6":
version "7.15.4"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz"
integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1":
+ version "7.16.0"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.0.tgz#e27b977f2e2088ba24748bf99b5e1dece64e4f0b"
+ integrity sha512-Nht8L0O8YCktmsDV6FqFue7vQLRx3Hb0B37lS5y0jDRqRxlBG4wIJHnf9/bgSE2UyipKFA01YtS+npRdTWBUyw==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
+"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.9.2":
+ version "7.15.3"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b"
+ integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
+"@babel/runtime@^7.8.4":
+ version "7.10.3"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364"
+ integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/template@^7.10.1", "@babel/template@^7.10.3":
version "7.10.3"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz"
@@ -1880,7 +1901,7 @@
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz"
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
-"@eslint/eslintrc@^0.4.3":
+"@eslint/eslintrc@^0.4.2":
version "0.4.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==
@@ -1910,20 +1931,6 @@
resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz"
integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==
-"@humanwhocodes/config-array@^0.5.0":
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
- integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==
- dependencies:
- "@humanwhocodes/object-schema" "^1.2.0"
- debug "^4.1.1"
- minimatch "^3.0.4"
-
-"@humanwhocodes/object-schema@^1.2.0":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
- integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
-
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz"
@@ -4009,6 +4016,7 @@ angular-moment-picker@^0.10.2:
dependencies:
angular-mocks "1.6.1"
angular-sanitize "1.6.1"
+ lodash-es "^4.17.15"
angular-resource@1.8.0:
version "1.8.0"
@@ -5665,7 +5673,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
-classnames@^2.3.1:
+classnames@2.x, classnames@^2.2.1, classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1:
version "2.3.1"
resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
@@ -6428,7 +6436,7 @@ cssesc@^3.0.0:
resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
-cssnano-preset-default@^4.0.8:
+cssnano-preset-default@^4.0.7:
version "4.0.8"
resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz#920622b1fc1e95a34e8838203f1397a504f2d3ff"
integrity sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==
@@ -7000,6 +7008,11 @@ dom-accessibility-api@^0.5.6:
resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.7.tgz"
integrity sha512-ml3lJIq9YjUfM9TUnEPvEYWFSwivwIGBPKpewX7tii7fwCazA8yCioGdqQcNsItPpfFvSJ3VIdMQPj60LJhcQA==
+dom-align@^1.7.0:
+ version "1.12.2"
+ resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.12.2.tgz#0f8164ebd0c9c21b0c790310493cd855892acd4b"
+ integrity sha512-pHuazgqrsTFrGU2WLDdXxCFabkdQDx72ddkraZNih1KsMcN5qsRSTR9O4VJRlwTPCPb5COYg3LOfiMHHcPInHg==
+
dom-converter@^0.2, dom-converter@^0.2.0:
version "0.2.0"
resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz"
@@ -13034,12 +13047,7 @@ node-readfiles@^0.2.0:
dependencies:
es6-promise "^3.2.1"
-node-releases@^1.1.53:
- version "1.1.58"
- resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.58.tgz#8ee20eef30fa60e52755fcc0942def5a734fe935"
- integrity sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg==
-
-node-releases@^1.1.61, node-releases@^1.1.77:
+node-releases@^1.1.61, node-releases@^1.1.76, node-releases@^1.1.77:
version "1.1.77"
resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.77.tgz"
integrity sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==
@@ -14838,6 +14846,66 @@ raw-loader@^4.0.2:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
+rc-align@^4.0.0:
+ version "4.0.11"
+ resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-4.0.11.tgz#8198c62db266bc1b8ef05e56c13275bf72628a5e"
+ integrity sha512-n9mQfIYQbbNTbefyQnRHZPWuTEwG1rY4a9yKlIWHSTbgwI+XUMGRYd0uJ5pE2UbrNX0WvnMBA1zJ3Lrecpra/A==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "2.x"
+ dom-align "^1.7.0"
+ lodash "^4.17.21"
+ rc-util "^5.3.0"
+ resize-observer-polyfill "^1.5.1"
+
+rc-motion@^2.0.0:
+ version "2.4.4"
+ resolved "https://registry.yarnpkg.com/rc-motion/-/rc-motion-2.4.4.tgz#e995d5fa24fc93065c24f714857cf2677d655bb0"
+ integrity sha512-ms7n1+/TZQBS0Ydd2Q5P4+wJTSOrhIrwNxLXCZpR7Fa3/oac7Yi803HDALc2hLAKaCTQtw9LmQeB58zcwOsqlQ==
+ dependencies:
+ "@babel/runtime" "^7.11.1"
+ classnames "^2.2.1"
+ rc-util "^5.2.1"
+
+rc-slider@^9.7.4:
+ version "9.7.4"
+ resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-9.7.4.tgz#430c860723bf6445ebf53517b550417a2f25eed1"
+ integrity sha512-pjLKLiDKiaL7/pNywfIBD+lDo5TtVo05KuIBSWEIoqu6FHh6IMWvthCiaODuYaVs3RLeF2nXOP5AjkD2Lt2Rwg==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "^2.2.5"
+ rc-tooltip "^5.0.1"
+ rc-util "^5.0.0"
+ shallowequal "^1.1.0"
+
+rc-tooltip@^5.0.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-5.1.1.tgz#94178ed162d0252bc4993b725f5dc2ac0fccf154"
+ integrity sha512-alt8eGMJulio6+4/uDm7nvV+rJq9bsfxFDCI0ljPdbuoygUscbsMYb6EQgwib/uqsXQUvzk+S7A59uYHmEgmDA==
+ dependencies:
+ "@babel/runtime" "^7.11.2"
+ rc-trigger "^5.0.0"
+
+rc-trigger@^5.0.0:
+ version "5.2.10"
+ resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-5.2.10.tgz#8a0057a940b1b9027eaa33beec8a6ecd85cce2b1"
+ integrity sha512-FkUf4H9BOFDaIwu42fvRycXMAvkttph9AlbCZXssZDVzz2L+QZ0ERvfB/4nX3ZFPh1Zd+uVGr1DEDeXxq4J1TA==
+ dependencies:
+ "@babel/runtime" "^7.11.2"
+ classnames "^2.2.6"
+ rc-align "^4.0.0"
+ rc-motion "^2.0.0"
+ rc-util "^5.5.0"
+
+rc-util@^5.0.0, rc-util@^5.2.1, rc-util@^5.3.0, rc-util@^5.5.0:
+ version "5.14.0"
+ resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.14.0.tgz#52c650e27570c2c47f7936c7d32eaec5212492a8"
+ integrity sha512-2vy6/Z1BJUcwLjm/UEJb/htjUTQPigITUIemCcFEo1fQevAumc9sA32x2z5qyWoa9uhrXbiAjSDpPIUqyg65sA==
+ dependencies:
+ "@babel/runtime" "^7.12.5"
+ react-is "^16.12.0"
+ shallowequal "^1.1.0"
+
react-colorful@^5.1.2:
version "5.5.0"
resolved "https://registry.npmjs.org/react-colorful/-/react-colorful-5.5.0.tgz"
@@ -14949,16 +15017,16 @@ react-inspector@^5.1.0:
is-dom "^1.0.0"
prop-types "^15.0.0"
+react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1:
+ version "16.13.1"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
+ integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+
"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2:
version "17.0.2"
resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
-react-is@^16.7.0, react-is@^16.8.1:
- version "16.13.1"
- resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
- integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
-
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz"
@@ -15474,6 +15542,11 @@ requires-port@^1.0.0:
resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
+resize-observer-polyfill@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+ integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz"
@@ -16547,7 +16620,7 @@ string-width@^3.0.0, string-width@^3.1.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
-string-width@^4.0.0, string-width@^4.2.3:
+string-width@^4.0.0:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==