mirror of
https://github.com/ansible/awx.git
synced 2026-05-07 09:27:36 -02:30
Merge pull request #4650 from keithjgrant/4431-jt-form-advanced-fields
4431 jt form advanced fields
Reviewed-by: Keith Grant
https://github.com/keithjgrant
This commit is contained in:
221
awx/ui_next/package-lock.json
generated
221
awx/ui_next/package-lock.json
generated
@@ -2072,9 +2072,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "11.13.4",
|
"version": "12.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.2.tgz",
|
||||||
"integrity": "sha512-+rabAZZ3Yn7tF/XPGHupKIL5EcAbrLxnTr/hgQICxbeuAfWtT0UZSfULE+ndusckBItcv4o6ZeOJplQikVcLvQ==",
|
"integrity": "sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/stack-utils": {
|
"@types/stack-utils": {
|
||||||
@@ -2338,13 +2338,13 @@
|
|||||||
"integrity": "sha512-ugTb7Lq7u4GfWSqqpwE0bGyoBZNMTok/zDBXxfEG0QM50jNlGhIWjRC1pPN7bvV1anhF+bs+/gNcRw+o55Evbg=="
|
"integrity": "sha512-ugTb7Lq7u4GfWSqqpwE0bGyoBZNMTok/zDBXxfEG0QM50jNlGhIWjRC1pPN7bvV1anhF+bs+/gNcRw+o55Evbg=="
|
||||||
},
|
},
|
||||||
"airbnb-prop-types": {
|
"airbnb-prop-types": {
|
||||||
"version": "2.13.2",
|
"version": "2.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.15.0.tgz",
|
||||||
"integrity": "sha512-2FN6DlHr6JCSxPPi25EnqGaXC4OC3/B3k1lCd6MMYrZ51/Gf/1qDfaR+JElzWa+Tl7cY2aYOlsYJGFeQyVHIeQ==",
|
"integrity": "sha512-jUh2/hfKsRjNFC4XONQrxo/n/3GG4Tn6Hl0WlFQN5PY9OMC9loSCoAYKnZsWaP8wEfd5xcrPloK0Zg6iS1xwVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"array.prototype.find": "^2.0.4",
|
"array.prototype.find": "^2.1.0",
|
||||||
"function.prototype.name": "^1.1.0",
|
"function.prototype.name": "^1.1.1",
|
||||||
"has": "^1.0.3",
|
"has": "^1.0.3",
|
||||||
"is-regex": "^1.0.4",
|
"is-regex": "^1.0.4",
|
||||||
"object-is": "^1.0.1",
|
"object-is": "^1.0.1",
|
||||||
@@ -2352,9 +2352,21 @@
|
|||||||
"object.entries": "^1.1.0",
|
"object.entries": "^1.1.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"prop-types-exact": "^1.2.0",
|
"prop-types-exact": "^1.2.0",
|
||||||
"react-is": "^16.8.6"
|
"react-is": "^16.9.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"function.prototype.name": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-e1NzkiJuw6xqVH7YSdiW/qDHebcmMhPNe6w+4ZYYEg0VA+LaLzx37RimbPLuonHhYGFGPx1ME2nSi74JiaCr/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"define-properties": "^1.1.3",
|
||||||
|
"function-bind": "^1.1.1",
|
||||||
|
"functions-have-names": "^1.1.1",
|
||||||
|
"is-callable": "^1.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"object.entries": {
|
"object.entries": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz",
|
||||||
@@ -2379,9 +2391,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "16.8.6",
|
"version": "16.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0.tgz",
|
||||||
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==",
|
"integrity": "sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2949,13 +2961,35 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"array.prototype.find": {
|
"array.prototype.find": {
|
||||||
"version": "2.0.4",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.1.0.tgz",
|
||||||
"integrity": "sha1-VWpcU2LAhkgyPdrrnenRS8GGTJA=",
|
"integrity": "sha512-Wn41+K1yuO5p7wRZDl7890c3xvv5UBrfVXTVIe28rSQb6LS0fZMDrQB6PAcxQFRFy6vJTLDc3A2+3CjQdzVKRg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"define-properties": "^1.1.2",
|
"define-properties": "^1.1.3",
|
||||||
"es-abstract": "^1.7.0"
|
"es-abstract": "^1.13.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"es-abstract": {
|
||||||
|
"version": "1.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz",
|
||||||
|
"integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"es-to-primitive": "^1.2.0",
|
||||||
|
"function-bind": "^1.1.1",
|
||||||
|
"has": "^1.0.3",
|
||||||
|
"is-callable": "^1.1.4",
|
||||||
|
"is-regex": "^1.0.4",
|
||||||
|
"object-keys": "^1.0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"object-keys": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"array.prototype.flat": {
|
"array.prototype.flat": {
|
||||||
@@ -5183,7 +5217,7 @@
|
|||||||
},
|
},
|
||||||
"css-select": {
|
"css-select": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
|
||||||
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
|
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -5886,9 +5920,9 @@
|
|||||||
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
|
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
|
||||||
},
|
},
|
||||||
"enzyme": {
|
"enzyme": {
|
||||||
"version": "3.9.0",
|
"version": "3.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.10.0.tgz",
|
||||||
"integrity": "sha512-JqxI2BRFHbmiP7/UFqvsjxTirWoM1HfeaJrmVSZ9a1EADKkZgdPcAuISPMpoUiHlac9J4dYt81MC5BBIrbJGMg==",
|
"integrity": "sha512-p2yy9Y7t/PFbPoTvrWde7JIYB2ZyGC+NgTNbVEGvZ5/EyoYSr9aG/2rSbVvyNvMHEhw9/dmGUJHWtfQIEiX9pg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"array.prototype.flat": "^1.2.1",
|
"array.prototype.flat": "^1.2.1",
|
||||||
@@ -5915,18 +5949,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enzyme-adapter-react-16": {
|
"enzyme-adapter-react-16": {
|
||||||
"version": "1.12.1",
|
"version": "1.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.14.0.tgz",
|
||||||
"integrity": "sha512-GB61gvY97XvrA6qljExGY+lgI6BBwz+ASLaRKct9VQ3ozu0EraqcNn3CcrUckSGIqFGa1+CxO5gj5is5t3lwrw==",
|
"integrity": "sha512-7PcOF7pb4hJUvjY7oAuPGpq3BmlCig3kxXGi2kFx0YzJHppqX1K8IIV9skT1IirxXlu8W7bneKi+oQ10QRnhcA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"enzyme-adapter-utils": "^1.11.0",
|
"enzyme-adapter-utils": "^1.12.0",
|
||||||
|
"has": "^1.0.3",
|
||||||
"object.assign": "^4.1.0",
|
"object.assign": "^4.1.0",
|
||||||
"object.values": "^1.1.0",
|
"object.values": "^1.1.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react-is": "^16.8.6",
|
"react-is": "^16.8.6",
|
||||||
"react-test-renderer": "^16.0.0-0",
|
"react-test-renderer": "^16.0.0-0",
|
||||||
"semver": "^5.6.0"
|
"semver": "^5.7.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prop-types": {
|
"prop-types": {
|
||||||
@@ -5941,20 +5976,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "16.8.6",
|
"version": "16.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0.tgz",
|
||||||
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==",
|
"integrity": "sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"semver": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enzyme-adapter-utils": {
|
"enzyme-adapter-utils": {
|
||||||
"version": "1.11.0",
|
"version": "1.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.0.tgz",
|
||||||
"integrity": "sha512-0VZeoE9MNx+QjTfsjmO1Mo+lMfunucYB4wt5ficU85WB/LoetTJrbuujmHP3PJx6pSoaAuLA+Mq877x4LoxdNg==",
|
"integrity": "sha512-wkZvE0VxcFx/8ZsBw0iAbk3gR1d9hK447ebnSYBf95+r32ezBq+XDSAvRErkc4LZosgH8J7et7H7/7CtUuQfBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"airbnb-prop-types": "^2.12.0",
|
"airbnb-prop-types": "^2.13.2",
|
||||||
"function.prototype.name": "^1.1.0",
|
"function.prototype.name": "^1.1.0",
|
||||||
"object.assign": "^4.1.0",
|
"object.assign": "^4.1.0",
|
||||||
"object.fromentries": "^2.0.0",
|
"object.fromentries": "^2.0.0",
|
||||||
@@ -5974,9 +6015,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "16.8.6",
|
"version": "16.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0.tgz",
|
||||||
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==",
|
"integrity": "sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7940,6 +7981,12 @@
|
|||||||
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
|
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"functions-have-names": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-U0kNHUoxwPNPWOJaMG7Z00d4a/qZVrFtzWJRaK8V9goaVOCXBSQSJpt3MYGNtkScKEBKovxLjnNdC9MlXwo5Pw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"fuzzaldrin": {
|
"fuzzaldrin": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fuzzaldrin/-/fuzzaldrin-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fuzzaldrin/-/fuzzaldrin-2.1.0.tgz",
|
||||||
@@ -8348,9 +8395,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"html-element-map": {
|
"html-element-map": {
|
||||||
"version": "1.0.1",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.1.0.tgz",
|
||||||
"integrity": "sha512-BZSfdEm6n706/lBfXKWa4frZRZcT5k1cOusw95ijZsHlI+GdgY0v95h6IzO3iIDf2ROwq570YTwqNPqHcNMozw==",
|
"integrity": "sha512-iqiG3dTZmy+uUaTmHarTL+3/A2VW9ox/9uasKEZC+R/wAtUrTcRlXPSaPqsnWPfIu8wqn09jQNwMRqzL54jSYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"array-filter": "^1.0.0"
|
"array-filter": "^1.0.0"
|
||||||
@@ -8396,9 +8443,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readable-stream": {
|
"readable-stream": {
|
||||||
"version": "3.3.0",
|
"version": "3.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
|
||||||
"integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==",
|
"integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
@@ -8406,13 +8453,19 @@
|
|||||||
"util-deprecate": "^1.0.1"
|
"util-deprecate": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"safe-buffer": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"string_decoder": {
|
"string_decoder": {
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
"integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==",
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11809,9 +11862,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"nearley": {
|
"nearley": {
|
||||||
"version": "2.16.0",
|
"version": "2.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/nearley/-/nearley-2.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/nearley/-/nearley-2.18.0.tgz",
|
||||||
"integrity": "sha512-Tr9XD3Vt/EujXbZBv6UAHYoLUSMQAxSsTnm9K3koXzjzNWY195NqALeyrzLZBKzAkL3gl92BcSogqrHjD8QuUg==",
|
"integrity": "sha512-/zQOMCeJcioI0xJtd5RpBiWw2WP7wLe6vq8/3Yu0rEwgus/G/+pViX80oA87JdVgjRt2895mZSv2VfZmy4W1uw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"commander": "^2.19.0",
|
"commander": "^2.19.0",
|
||||||
@@ -13376,32 +13429,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-test-renderer": {
|
"react-test-renderer": {
|
||||||
"version": "16.8.6",
|
"version": "16.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.9.0.tgz",
|
||||||
"integrity": "sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw==",
|
"integrity": "sha512-R62stB73qZyhrJo7wmCW9jgl/07ai+YzvouvCXIJLBkRlRqLx4j9RqcLEAfNfU3OxTGucqR2Whmn3/Aad6L3hQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react-is": "^16.8.6",
|
"react-is": "^16.9.0",
|
||||||
"scheduler": "^0.13.6"
|
"scheduler": "^0.15.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "16.8.6",
|
"version": "16.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0.tgz",
|
||||||
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==",
|
"integrity": "sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
|
||||||
"scheduler": {
|
|
||||||
"version": "0.13.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz",
|
|
||||||
"integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"loose-envify": "^1.1.0",
|
|
||||||
"object-assign": "^4.1.1"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -14364,6 +14407,16 @@
|
|||||||
"object-assign": "^4.1.1"
|
"object-assign": "^4.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"scheduler": {
|
||||||
|
"version": "0.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.15.0.tgz",
|
||||||
|
"integrity": "sha512-xAefmSfN6jqAa7Kuq7LIJY0bwAPG3xlCj0HMEBQk1lxYiDKZscY2xJ5U/61ZTrYbmNQbXa+gc7czPkVo11tnCg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"loose-envify": "^1.1.0",
|
||||||
|
"object-assign": "^4.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"schema-utils": {
|
"schema-utils": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
|
||||||
@@ -15250,14 +15303,36 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"string.prototype.trim": {
|
"string.prototype.trim": {
|
||||||
"version": "1.1.2",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.0.tgz",
|
||||||
"integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=",
|
"integrity": "sha512-9EIjYD/WdlvLpn987+ctkLf0FfvBefOCuiEr2henD8X+7jfwPnyvTdmW8OJhj5p+M0/96mBdynLWkxUr+rHlpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"define-properties": "^1.1.2",
|
"define-properties": "^1.1.3",
|
||||||
"es-abstract": "^1.5.0",
|
"es-abstract": "^1.13.0",
|
||||||
"function-bind": "^1.0.2"
|
"function-bind": "^1.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"es-abstract": {
|
||||||
|
"version": "1.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz",
|
||||||
|
"integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"es-to-primitive": "^1.2.0",
|
||||||
|
"function-bind": "^1.1.1",
|
||||||
|
"has": "^1.0.3",
|
||||||
|
"is-callable": "^1.1.4",
|
||||||
|
"is-regex": "^1.0.4",
|
||||||
|
"object-keys": "^1.0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"object-keys": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"string_decoder": {
|
"string_decoder": {
|
||||||
|
|||||||
@@ -33,8 +33,8 @@
|
|||||||
"babel-plugin-macros": "^2.4.2",
|
"babel-plugin-macros": "^2.4.2",
|
||||||
"babel-plugin-styled-components": "^1.10.0",
|
"babel-plugin-styled-components": "^1.10.0",
|
||||||
"css-loader": "^1.0.0",
|
"css-loader": "^1.0.0",
|
||||||
"enzyme": "^3.9.0",
|
"enzyme": "^3.10.0",
|
||||||
"enzyme-adapter-react-16": "^1.12.1",
|
"enzyme-adapter-react-16": "^1.14.0",
|
||||||
"enzyme-to-json": "^3.3.5",
|
"enzyme-to-json": "^3.3.5",
|
||||||
"eslint": "^5.6.0",
|
"eslint": "^5.6.0",
|
||||||
"eslint-config-airbnb": "^17.1.0",
|
"eslint-config-airbnb": "^17.1.0",
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import {
|
||||||
|
arrayOf,
|
||||||
|
oneOfType,
|
||||||
|
func,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
shape,
|
||||||
|
bool,
|
||||||
|
} from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { FormSelect, FormSelectOption } from '@patternfly/react-core';
|
import { FormSelect, FormSelectOption } from '@patternfly/react-core';
|
||||||
@@ -48,12 +56,12 @@ AnsibleSelect.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
AnsibleSelect.propTypes = {
|
AnsibleSelect.propTypes = {
|
||||||
data: PropTypes.arrayOf(PropTypes.object),
|
data: arrayOf(shape()),
|
||||||
id: PropTypes.string.isRequired,
|
id: string.isRequired,
|
||||||
isValid: PropTypes.bool,
|
isValid: bool,
|
||||||
onBlur: PropTypes.func,
|
onBlur: func,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: func.isRequired,
|
||||||
value: PropTypes.string.isRequired,
|
value: oneOfType([string, number]).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { AnsibleSelect as _AnsibleSelect };
|
export { AnsibleSelect as _AnsibleSelect };
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { bool, string } from 'prop-types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Button } from '@patternfly/react-core';
|
||||||
|
import { AngleRightIcon } from '@patternfly/react-icons';
|
||||||
|
import omitProps from '@util/omitProps';
|
||||||
|
import ExpandingContainer from './ExpandingContainer';
|
||||||
|
|
||||||
|
const Toggle = styled.div`
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin-left: 20px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-self: center;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid var(--pf-global--BorderColor--300);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Arrow = styled(omitProps(AngleRightIcon, 'isExpanded'))`
|
||||||
|
margin-right: -5px;
|
||||||
|
margin-left: 5px;
|
||||||
|
transition: transform 0.1s ease-out;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
${props => props.isExpanded && `transform: rotate(90deg);`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function CollapsibleSection({ label, startExpanded, children }) {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(startExpanded);
|
||||||
|
const toggle = () => setIsExpanded(!isExpanded);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Toggle>
|
||||||
|
<Button onClick={toggle}>
|
||||||
|
{label} <Arrow isExpanded={isExpanded} />
|
||||||
|
</Button>
|
||||||
|
<hr />
|
||||||
|
</Toggle>
|
||||||
|
<ExpandingContainer isExpanded={isExpanded}>
|
||||||
|
{children}
|
||||||
|
</ExpandingContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
CollapsibleSection.propTypes = {
|
||||||
|
label: string.isRequired,
|
||||||
|
startExpanded: bool,
|
||||||
|
};
|
||||||
|
CollapsibleSection.defaultProps = {
|
||||||
|
startExpanded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollapsibleSection;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import CollapsibleSection from './CollapsibleSection';
|
||||||
|
|
||||||
|
describe('<CollapsibleSection>', () => {
|
||||||
|
it('should render contents', () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<CollapsibleSection label="Advanced">foo</CollapsibleSection>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('Button > *').prop('isExpanded')).toEqual(false);
|
||||||
|
expect(wrapper.find('ExpandingContainer').prop('isExpanded')).toEqual(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(wrapper.find('ExpandingContainer').prop('children')).toEqual('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle when clicked', () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<CollapsibleSection label="Advanced">foo</CollapsibleSection>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('Button > *').prop('isExpanded')).toEqual(false);
|
||||||
|
wrapper.find('Button').simulate('click');
|
||||||
|
expect(wrapper.find('Button > *').prop('isExpanded')).toEqual(true);
|
||||||
|
expect(wrapper.find('ExpandingContainer').prop('isExpanded')).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { bool } from 'prop-types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
margin: 15px 0;
|
||||||
|
transition: height 0.2s ease-out;
|
||||||
|
${props => props.hideOverflow && `overflow: hidden;`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function ExpandingContainer({ isExpanded, children }) {
|
||||||
|
const [contentHeight, setContentHeight] = useState(0);
|
||||||
|
const [hideOverflow, setHideOverflow] = useState(!isExpanded);
|
||||||
|
const ref = useRef(null);
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current.addEventListener('transitionend', () => {
|
||||||
|
setHideOverflow(!isExpanded);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
setContentHeight(ref.current.scrollHeight);
|
||||||
|
});
|
||||||
|
const height = isExpanded ? contentHeight : '0';
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
ref={ref}
|
||||||
|
css={`
|
||||||
|
height: ${height}px;
|
||||||
|
`}
|
||||||
|
hideOverflow={!isExpanded || hideOverflow}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ExpandingContainer.propTypes = {
|
||||||
|
isExpanded: bool,
|
||||||
|
};
|
||||||
|
ExpandingContainer.defaultProps = {
|
||||||
|
isExpanded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpandingContainer;
|
||||||
1
awx/ui_next/src/components/CollapsibleSection/index.js
Normal file
1
awx/ui_next/src/components/CollapsibleSection/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './CollapsibleSection';
|
||||||
@@ -29,6 +29,8 @@ const ToolbarItem = styled(PFToolbarItem)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// TODO: Recommend renaming this component to avoid confusion
|
||||||
|
// with ExpandingContainer
|
||||||
class ExpandCollapse extends React.Component {
|
class ExpandCollapse extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const { isCompact, onCompact, onExpand, i18n } = this.props;
|
const { isCompact, onCompact, onExpand, i18n } = this.props;
|
||||||
|
|||||||
55
awx/ui_next/src/components/FormField/CheckboxField.jsx
Normal file
55
awx/ui_next/src/components/FormField/CheckboxField.jsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { string, func } from 'prop-types';
|
||||||
|
import { Field } from 'formik';
|
||||||
|
import { Checkbox, Tooltip } from '@patternfly/react-core';
|
||||||
|
import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
||||||
|
margin-left: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function CheckboxField({ id, name, label, tooltip, validate, ...rest }) {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
name={name}
|
||||||
|
validate={validate}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
aria-label={label}
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
{label}
|
||||||
|
|
||||||
|
{tooltip && (
|
||||||
|
<Tooltip position="right" content={tooltip}>
|
||||||
|
<QuestionCircleIcon />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
id={id}
|
||||||
|
{...rest}
|
||||||
|
isChecked={field.value}
|
||||||
|
{...field}
|
||||||
|
onChange={(value, event) => {
|
||||||
|
field.onChange(event);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
CheckboxField.propTypes = {
|
||||||
|
id: string.isRequired,
|
||||||
|
name: string.isRequired,
|
||||||
|
label: string.isRequired,
|
||||||
|
validate: func,
|
||||||
|
tooltip: string,
|
||||||
|
};
|
||||||
|
CheckboxField.defaultProps = {
|
||||||
|
validate: () => {},
|
||||||
|
tooltip: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CheckboxField;
|
||||||
@@ -57,7 +57,7 @@ FormField.propTypes = {
|
|||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
validate: PropTypes.func,
|
validate: PropTypes.func,
|
||||||
isRequired: PropTypes.bool,
|
isRequired: PropTypes.bool,
|
||||||
tooltip: PropTypes.string,
|
tooltip: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
FormField.defaultProps = {
|
FormField.defaultProps = {
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export { default } from './FormField';
|
export { default } from './FormField';
|
||||||
|
export { default as CheckboxField } from './CheckboxField';
|
||||||
|
|||||||
82
awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx
Normal file
82
awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { FormGroup, Tooltip } from '@patternfly/react-core';
|
||||||
|
import { QuestionCircleIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
import { InstanceGroupsAPI } from '@api';
|
||||||
|
import Lookup from '@components/Lookup';
|
||||||
|
|
||||||
|
const getInstanceGroups = async params => InstanceGroupsAPI.read(params);
|
||||||
|
|
||||||
|
class InstanceGroupsLookup extends React.Component {
|
||||||
|
render() {
|
||||||
|
const { value, tooltip, onChange, className, i18n } = this.props;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Wrapping <div> added to workaround PF bug:
|
||||||
|
https://github.com/patternfly/patternfly-react/issues/2855
|
||||||
|
*/
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<FormGroup
|
||||||
|
label={
|
||||||
|
<Fragment>
|
||||||
|
{i18n._(t`Instance Groups`)}{' '}
|
||||||
|
{tooltip && (
|
||||||
|
<Tooltip position="right" content={tooltip}>
|
||||||
|
<QuestionCircleIcon />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
fieldId="org-instance-groups"
|
||||||
|
>
|
||||||
|
<Lookup
|
||||||
|
id="org-instance-groups"
|
||||||
|
lookupHeader={i18n._(t`Instance Groups`)}
|
||||||
|
name="instanceGroups"
|
||||||
|
value={value}
|
||||||
|
onLookupSave={onChange}
|
||||||
|
getItems={getInstanceGroups}
|
||||||
|
multiple
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Name`),
|
||||||
|
key: 'name',
|
||||||
|
isSortable: true,
|
||||||
|
isSearchable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Modified`),
|
||||||
|
key: 'modified',
|
||||||
|
isSortable: false,
|
||||||
|
isNumeric: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Created`),
|
||||||
|
key: 'created',
|
||||||
|
isSortable: false,
|
||||||
|
isNumeric: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
sortedColumnKey="name"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InstanceGroupsLookup.propTypes = {
|
||||||
|
value: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
tooltip: PropTypes.string,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
InstanceGroupsLookup.defaultProps = {
|
||||||
|
tooltip: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(InstanceGroupsLookup);
|
||||||
@@ -27,6 +27,7 @@ class InventoriesLookup extends React.Component {
|
|||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
}
|
||||||
|
isRequired={required}
|
||||||
fieldId="inventories-lookup"
|
fieldId="inventories-lookup"
|
||||||
>
|
>
|
||||||
<Lookup
|
<Lookup
|
||||||
@@ -1 +1,3 @@
|
|||||||
export { default } from './Lookup';
|
export { default } from './Lookup';
|
||||||
|
export { default as InstanceGroupsLookup } from './InstanceGroupsLookup';
|
||||||
|
export { default as InventoriesLookup } from './InventoriesLookup';
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { shape, number, string, func, arrayOf, oneOfType } from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
import { Chip, ChipGroup } from '@components/Chip';
|
import { Chip, ChipGroup } from '@components/Chip';
|
||||||
import {
|
import {
|
||||||
Dropdown as PFDropdown,
|
Dropdown as PFDropdown,
|
||||||
@@ -15,11 +13,13 @@ const InputGroup = styled.div`
|
|||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const TextInput = styled(PFTextInput)`
|
const TextInput = styled(PFTextInput)`
|
||||||
border: none;
|
border: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Dropdown = styled(PFDropdown)`
|
const Dropdown = styled(PFDropdown)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
.pf-c-dropdown__toggle.pf-m-plain {
|
.pf-c-dropdown__toggle.pf-m-plain {
|
||||||
@@ -38,15 +38,27 @@ const Dropdown = styled(PFDropdown)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const Item = shape({
|
||||||
|
id: oneOfType([number, string]).isRequired,
|
||||||
|
name: string.isRequired,
|
||||||
|
});
|
||||||
|
|
||||||
class MultiSelect extends Component {
|
class MultiSelect extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
associatedItems: PropTypes.arrayOf(
|
associatedItems: arrayOf(Item).isRequired,
|
||||||
PropTypes.shape({
|
options: arrayOf(Item),
|
||||||
name: PropTypes.string.isRequired,
|
onAddNewItem: func,
|
||||||
})
|
onRemoveItem: func,
|
||||||
).isRequired,
|
onChange: func,
|
||||||
onAddNewItem: PropTypes.func.isRequired,
|
createNewItem: func,
|
||||||
onRemoveItem: PropTypes.func.isRequired,
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onAddNewItem: () => {},
|
||||||
|
onRemoveItem: () => {},
|
||||||
|
onChange: () => {},
|
||||||
|
options: [],
|
||||||
|
createNewItem: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -61,6 +73,7 @@ class MultiSelect extends Component {
|
|||||||
this.handleSelection = this.handleSelection.bind(this);
|
this.handleSelection = this.handleSelection.bind(this);
|
||||||
this.removeChip = this.removeChip.bind(this);
|
this.removeChip = this.removeChip.bind(this);
|
||||||
this.handleClick = this.handleClick.bind(this);
|
this.handleClick = this.handleClick.bind(this);
|
||||||
|
this.createNewItem = this.createNewItem.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -73,11 +86,7 @@ class MultiSelect extends Component {
|
|||||||
|
|
||||||
getInitialChipItems() {
|
getInitialChipItems() {
|
||||||
const { associatedItems } = this.props;
|
const { associatedItems } = this.props;
|
||||||
return associatedItems.map(item => ({
|
return associatedItems.map(item => ({ ...item }));
|
||||||
name: item.name,
|
|
||||||
id: item.id,
|
|
||||||
organization: item.organization,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick(e, option) {
|
handleClick(e, option) {
|
||||||
@@ -92,19 +101,33 @@ class MultiSelect extends Component {
|
|||||||
|
|
||||||
handleSelection(e, item) {
|
handleSelection(e, item) {
|
||||||
const { chipItems } = this.state;
|
const { chipItems } = this.state;
|
||||||
const { onAddNewItem } = this.props;
|
const { onAddNewItem, onChange } = this.props;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
const items = chipItems.concat({ name: item.name, id: item.id });
|
||||||
this.setState({
|
this.setState({
|
||||||
chipItems: chipItems.concat({ name: item.name, id: item.id }),
|
chipItems: items,
|
||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
});
|
});
|
||||||
onAddNewItem(item);
|
onAddNewItem(item);
|
||||||
|
onChange(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
createNewItem(name) {
|
||||||
|
const { createNewItem } = this.props;
|
||||||
|
if (createNewItem) {
|
||||||
|
return createNewItem(name);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: Math.random(),
|
||||||
|
name,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAddItem(event) {
|
handleAddItem(event) {
|
||||||
const { input, chipItems } = this.state;
|
const { input, chipItems } = this.state;
|
||||||
const { onAddNewItem } = this.props;
|
const { options, onAddNewItem, onChange } = this.props;
|
||||||
|
const match = options.find(item => item.name === input);
|
||||||
const isIncluded = chipItems.some(chipItem => chipItem.name === input);
|
const isIncluded = chipItems.some(chipItem => chipItem.name === input);
|
||||||
|
|
||||||
if (!input) {
|
if (!input) {
|
||||||
@@ -118,30 +141,35 @@ class MultiSelect extends Component {
|
|||||||
this.setState({ input: '', isExpanded: false });
|
this.setState({ input: '', isExpanded: false });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === 'Enter') {
|
const isNewItem = !match || !chipItems.find(item => item.id === match.id);
|
||||||
|
if (event.key === 'Enter' && isNewItem) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
const items = chipItems.concat({ name: input, id: input });
|
||||||
|
const newItem = match || this.createNewItem(input);
|
||||||
this.setState({
|
this.setState({
|
||||||
chipItems: chipItems.concat({ name: input, id: input }),
|
chipItems: items,
|
||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
input: '',
|
input: '',
|
||||||
});
|
});
|
||||||
onAddNewItem(input);
|
onAddNewItem(newItem);
|
||||||
} else if (event.key === 'Tab') {
|
onChange(items);
|
||||||
this.setState({ input: '' });
|
} else if (!isNewItem || event.key === 'Tab') {
|
||||||
|
this.setState({ isExpanded: false, input: '' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInputChange(e) {
|
handleInputChange(value) {
|
||||||
this.setState({ input: e, isExpanded: true });
|
this.setState({ input: value, isExpanded: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
removeChip(e, item) {
|
removeChip(e, item) {
|
||||||
const { onRemoveItem } = this.props;
|
const { onRemoveItem, onChange } = this.props;
|
||||||
const { chipItems } = this.state;
|
const { chipItems } = this.state;
|
||||||
const chips = chipItems.filter(chip => chip.id !== item.id);
|
const chips = chipItems.filter(chip => chip.id !== item.id);
|
||||||
|
|
||||||
this.setState({ chipItems: chips });
|
this.setState({ chipItems: chips });
|
||||||
onRemoveItem(item);
|
onRemoveItem(item);
|
||||||
|
onChange(chips);
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -214,5 +242,4 @@ class MultiSelect extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export { MultiSelect as _MultiSelect };
|
export default MultiSelect;
|
||||||
export default withI18n()(withRouter(MultiSelect));
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
import { sleep } from '@testUtils/testUtils';
|
import { sleep } from '@testUtils/testUtils';
|
||||||
import MultiSelect, { _MultiSelect } from './MultiSelect';
|
import MultiSelect from './MultiSelect';
|
||||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|
||||||
|
|
||||||
describe('<MultiSelect />', () => {
|
describe('<MultiSelect />', () => {
|
||||||
const associatedItems = [
|
const associatedItems = [
|
||||||
@@ -11,11 +11,7 @@ describe('<MultiSelect />', () => {
|
|||||||
const options = [{ name: 'Angry', id: 3 }, { name: 'Potato', id: 4 }];
|
const options = [{ name: 'Angry', id: 3 }, { name: 'Potato', id: 4 }];
|
||||||
|
|
||||||
test('Initially render successfully', () => {
|
test('Initially render successfully', () => {
|
||||||
const getInitialChipItems = jest.spyOn(
|
const wrapper = mount(
|
||||||
_MultiSelect.prototype,
|
|
||||||
'getInitialChipItems'
|
|
||||||
);
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
onAddNewItem={jest.fn()}
|
onAddNewItem={jest.fn()}
|
||||||
onRemoveItem={jest.fn()}
|
onRemoveItem={jest.fn()}
|
||||||
@@ -25,11 +21,11 @@ describe('<MultiSelect />', () => {
|
|||||||
);
|
);
|
||||||
const component = wrapper.find('MultiSelect');
|
const component = wrapper.find('MultiSelect');
|
||||||
|
|
||||||
expect(getInitialChipItems).toBeCalled();
|
|
||||||
expect(component.state().chipItems.length).toBe(2);
|
expect(component.state().chipItems.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleSelection add item to chipItems', async () => {
|
test('handleSelection add item to chipItems', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mount(
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
onAddNewItem={jest.fn()}
|
onAddNewItem={jest.fn()}
|
||||||
onRemoveItem={jest.fn()}
|
onRemoveItem={jest.fn()}
|
||||||
@@ -45,12 +41,15 @@ describe('<MultiSelect />', () => {
|
|||||||
await sleep(1);
|
await sleep(1);
|
||||||
expect(component.state().chipItems.length).toBe(2);
|
expect(component.state().chipItems.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleAddItem adds a chip only when Tab is pressed', () => {
|
test('handleAddItem adds a chip only when Tab is pressed', () => {
|
||||||
const onAddNewItem = jest.fn();
|
const onAddNewItem = jest.fn();
|
||||||
const wrapper = mountWithContexts(
|
const onChange = jest.fn();
|
||||||
|
const wrapper = mount(
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
onAddNewItem={onAddNewItem}
|
onAddNewItem={onAddNewItem}
|
||||||
onRemoveItem={jest.fn()}
|
onRemoveItem={jest.fn()}
|
||||||
|
onChange={onChange}
|
||||||
associatedItems={associatedItems}
|
associatedItems={associatedItems}
|
||||||
options={options}
|
options={options}
|
||||||
/>
|
/>
|
||||||
@@ -68,14 +67,18 @@ describe('<MultiSelect />', () => {
|
|||||||
expect(component.state().input.length).toBe(0);
|
expect(component.state().input.length).toBe(0);
|
||||||
expect(component.state().isExpanded).toBe(false);
|
expect(component.state().isExpanded).toBe(false);
|
||||||
expect(onAddNewItem).toBeCalled();
|
expect(onAddNewItem).toBeCalled();
|
||||||
|
expect(onChange).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('removeChip removes chip properly', () => {
|
test('removeChip removes chip properly', () => {
|
||||||
const onRemoveItem = jest.fn();
|
const onRemoveItem = jest.fn();
|
||||||
|
const onChange = jest.fn();
|
||||||
|
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mount(
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
onAddNewItem={jest.fn()}
|
onAddNewItem={jest.fn()}
|
||||||
onRemoveItem={onRemoveItem}
|
onRemoveItem={onRemoveItem}
|
||||||
|
onChange={onChange}
|
||||||
associatedItems={associatedItems}
|
associatedItems={associatedItems}
|
||||||
options={options}
|
options={options}
|
||||||
/>
|
/>
|
||||||
@@ -89,5 +92,6 @@ describe('<MultiSelect />', () => {
|
|||||||
.removeChip(event, { name: 'Foo', id: 1, organization: 1 });
|
.removeChip(event, { name: 'Foo', id: 1, organization: 1 });
|
||||||
expect(component.state().chipItems.length).toBe(1);
|
expect(component.state().chipItems.length).toBe(1);
|
||||||
expect(onRemoveItem).toBeCalled();
|
expect(onRemoveItem).toBeCalled();
|
||||||
|
expect(onChange).toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
48
awx/ui_next/src/components/MultiSelect/TagMultiSelect.jsx
Normal file
48
awx/ui_next/src/components/MultiSelect/TagMultiSelect.jsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { func, string } from 'prop-types';
|
||||||
|
import MultiSelect from './MultiSelect';
|
||||||
|
|
||||||
|
function arrayToString(tags) {
|
||||||
|
return tags.map(v => v.name).join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringToArray(value) {
|
||||||
|
return value
|
||||||
|
.split(',')
|
||||||
|
.filter(val => !!val)
|
||||||
|
.map(val => ({
|
||||||
|
id: val,
|
||||||
|
name: val,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Adapter providing a simplified API to a MultiSelect. The value
|
||||||
|
* is a comma-separated string.
|
||||||
|
*/
|
||||||
|
function TagMultiSelect({ onChange, value }) {
|
||||||
|
const [options, setOptions] = useState(stringToArray(value));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MultiSelect
|
||||||
|
onChange={val => {
|
||||||
|
onChange(arrayToString(val));
|
||||||
|
}}
|
||||||
|
onAddNewItem={newItem => {
|
||||||
|
if (!options.find(o => o.name === newItem.name)) {
|
||||||
|
setOptions(options.concat(newItem));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
associatedItems={stringToArray(value)}
|
||||||
|
options={options}
|
||||||
|
createNewItem={name => ({ id: name, name })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TagMultiSelect.propTypes = {
|
||||||
|
onChange: func.isRequired,
|
||||||
|
value: string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TagMultiSelect;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
import TagMultiSelect from './TagMultiSelect';
|
||||||
|
|
||||||
|
describe('<TagMultiSelect />', () => {
|
||||||
|
it('should render MultiSelect', () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<TagMultiSelect value="foo,bar" onChange={jest.fn()} />
|
||||||
|
);
|
||||||
|
expect(wrapper.find('MultiSelect').prop('options')).toEqual([
|
||||||
|
{ id: 'foo', name: 'foo' },
|
||||||
|
{ id: 'bar', name: 'bar' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not treat empty string as an option', () => {
|
||||||
|
const wrapper = mount(<TagMultiSelect value="" onChange={jest.fn()} />);
|
||||||
|
expect(wrapper.find('MultiSelect').prop('options')).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger onChange', () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const wrapper = mount(
|
||||||
|
<TagMultiSelect value="foo,bar" onChange={onChange} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const select = wrapper.find('MultiSelect');
|
||||||
|
select.invoke('onChange')([
|
||||||
|
{ name: 'foo' },
|
||||||
|
{ name: 'bar' },
|
||||||
|
{ name: 'baz' },
|
||||||
|
]);
|
||||||
|
expect(onChange).toHaveBeenCalledWith('foo,bar,baz');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1 +1,2 @@
|
|||||||
export { default } from './MultiSelect';
|
export { default } from './MultiSelect';
|
||||||
|
export { default as TagMultiSelect } from './TagMultiSelect';
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import React, { Fragment } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import { FormGroup, Tooltip } from '@patternfly/react-core';
|
|
||||||
import { QuestionCircleIcon } from '@patternfly/react-icons';
|
|
||||||
|
|
||||||
import { InstanceGroupsAPI } from '@api';
|
|
||||||
import Lookup from '@components/Lookup';
|
|
||||||
|
|
||||||
const getInstanceGroups = async params => InstanceGroupsAPI.read(params);
|
|
||||||
|
|
||||||
class InstanceGroupsLookup extends React.Component {
|
|
||||||
render() {
|
|
||||||
const { value, tooltip, onChange, i18n } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormGroup
|
|
||||||
label={
|
|
||||||
<Fragment>
|
|
||||||
{i18n._(t`Instance Groups`)}{' '}
|
|
||||||
{tooltip && (
|
|
||||||
<Tooltip position="right" content={tooltip}>
|
|
||||||
<QuestionCircleIcon />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
}
|
|
||||||
fieldId="org-instance-groups"
|
|
||||||
>
|
|
||||||
<Lookup
|
|
||||||
id="org-instance-groups"
|
|
||||||
lookupHeader={i18n._(t`Instance Groups`)}
|
|
||||||
name="instanceGroups"
|
|
||||||
value={value}
|
|
||||||
onLookupSave={onChange}
|
|
||||||
getItems={getInstanceGroups}
|
|
||||||
multiple
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
name: i18n._(t`Name`),
|
|
||||||
key: 'name',
|
|
||||||
isSortable: true,
|
|
||||||
isSearchable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n._(t`Modified`),
|
|
||||||
key: 'modified',
|
|
||||||
isSortable: false,
|
|
||||||
isNumeric: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n._(t`Created`),
|
|
||||||
key: 'created',
|
|
||||||
isSortable: false,
|
|
||||||
isNumeric: true,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
sortedColumnKey="name"
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
InstanceGroupsLookup.propTypes = {
|
|
||||||
value: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
tooltip: PropTypes.string,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
InstanceGroupsLookup.defaultProps = {
|
|
||||||
tooltip: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withI18n()(InstanceGroupsLookup);
|
|
||||||
@@ -15,10 +15,9 @@ import FormRow from '@components/FormRow';
|
|||||||
import FormField from '@components/FormField';
|
import FormField from '@components/FormField';
|
||||||
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
|
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
|
||||||
import AnsibleSelect from '@components/AnsibleSelect';
|
import AnsibleSelect from '@components/AnsibleSelect';
|
||||||
|
import { InstanceGroupsLookup } from '@components/Lookup/';
|
||||||
import { required, minMaxValue } from '@util/validators';
|
import { required, minMaxValue } from '@util/validators';
|
||||||
|
|
||||||
import InstanceGroupsLookup from './InstanceGroupsLookup';
|
|
||||||
|
|
||||||
class OrganizationForm extends Component {
|
class OrganizationForm extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { default as InstanceGroupsLookup } from './InstanceGroupsLookup';
|
/* eslint-disable-next-line import/prefer-default-export */
|
||||||
export { default as OrganizationForm } from './OrganizationForm';
|
export { default as OrganizationForm } from './OrganizationForm';
|
||||||
|
|||||||
@@ -17,23 +17,30 @@ function JobTemplateAdd({ history, i18n }) {
|
|||||||
const [formSubmitError, setFormSubmitError] = useState(null);
|
const [formSubmitError, setFormSubmitError] = useState(null);
|
||||||
|
|
||||||
async function handleSubmit(values) {
|
async function handleSubmit(values) {
|
||||||
const { newLabels, removedLabels } = values;
|
const {
|
||||||
delete values.newLabels;
|
newLabels,
|
||||||
delete values.removedLabels;
|
removedLabels,
|
||||||
|
addedInstanceGroups,
|
||||||
|
removedInstanceGroups,
|
||||||
|
...remainingValues
|
||||||
|
} = values;
|
||||||
|
|
||||||
setFormSubmitError(null);
|
setFormSubmitError(null);
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: { id, type },
|
data: { id, type },
|
||||||
} = await JobTemplatesAPI.create(values);
|
} = await JobTemplatesAPI.create(remainingValues);
|
||||||
await Promise.all([submitLabels(id, newLabels, removedLabels)]);
|
await Promise.all([
|
||||||
|
submitLabels(id, newLabels, removedLabels),
|
||||||
|
submitInstanceGroups(id, addedInstanceGroups, removedInstanceGroups),
|
||||||
|
]);
|
||||||
history.push(`/templates/${type}/${id}/details`);
|
history.push(`/templates/${type}/${id}/details`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFormSubmitError(error);
|
setFormSubmitError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitLabels(id, newLabels = [], removedLabels = []) {
|
function submitLabels(id, newLabels = [], removedLabels = []) {
|
||||||
const disassociationPromises = removedLabels.map(label =>
|
const disassociationPromises = removedLabels.map(label =>
|
||||||
JobTemplatesAPI.disassociateLabel(id, label)
|
JobTemplatesAPI.disassociateLabel(id, label)
|
||||||
);
|
);
|
||||||
@@ -44,12 +51,18 @@ function JobTemplateAdd({ history, i18n }) {
|
|||||||
.filter(label => label.organization)
|
.filter(label => label.organization)
|
||||||
.map(label => JobTemplatesAPI.generateLabel(id, label));
|
.map(label => JobTemplatesAPI.generateLabel(id, label));
|
||||||
|
|
||||||
const results = await Promise.all([
|
return Promise.all([
|
||||||
...disassociationPromises,
|
...disassociationPromises,
|
||||||
...associationPromises,
|
...associationPromises,
|
||||||
...creationPromises,
|
...creationPromises,
|
||||||
]);
|
]);
|
||||||
return results;
|
}
|
||||||
|
|
||||||
|
function submitInstanceGroups(templateId, addedGroups = []) {
|
||||||
|
const associatePromises = addedGroups.map(group =>
|
||||||
|
JobTemplatesAPI.associateInstanceGroup(templateId, group.id)
|
||||||
|
);
|
||||||
|
return Promise.all(associatePromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
|
|||||||
@@ -6,6 +6,27 @@ import { JobTemplatesAPI, LabelsAPI } from '@api';
|
|||||||
|
|
||||||
jest.mock('@api');
|
jest.mock('@api');
|
||||||
|
|
||||||
|
const jobTemplateData = {
|
||||||
|
name: 'Foo',
|
||||||
|
description: 'Baz',
|
||||||
|
job_type: 'run',
|
||||||
|
inventory: 1,
|
||||||
|
project: 2,
|
||||||
|
playbook: 'Bar',
|
||||||
|
forks: 0,
|
||||||
|
limit: '',
|
||||||
|
verbosity: '0',
|
||||||
|
job_slice_count: 1,
|
||||||
|
timeout: 0,
|
||||||
|
job_tags: '',
|
||||||
|
skip_tags: '',
|
||||||
|
diff_mode: false,
|
||||||
|
allow_callbacks: false,
|
||||||
|
allow_simultaneous: false,
|
||||||
|
use_fact_cache: false,
|
||||||
|
host_config_key: '',
|
||||||
|
};
|
||||||
|
|
||||||
describe('<JobTemplateAdd />', () => {
|
describe('<JobTemplateAdd />', () => {
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
description: '',
|
description: '',
|
||||||
@@ -63,14 +84,6 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('handleSubmit should post to api', async done => {
|
test('handleSubmit should post to api', async done => {
|
||||||
const jobTemplateData = {
|
|
||||||
description: 'Baz',
|
|
||||||
inventory: 1,
|
|
||||||
job_type: 'run',
|
|
||||||
name: 'Foo',
|
|
||||||
playbook: 'Bar',
|
|
||||||
project: 2,
|
|
||||||
};
|
|
||||||
JobTemplatesAPI.create.mockResolvedValueOnce({
|
JobTemplatesAPI.create.mockResolvedValueOnce({
|
||||||
data: {
|
data: {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -99,14 +112,6 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
const history = {
|
const history = {
|
||||||
push: jest.fn(),
|
push: jest.fn(),
|
||||||
};
|
};
|
||||||
const jobTemplateData = {
|
|
||||||
description: 'Baz',
|
|
||||||
inventory: 1,
|
|
||||||
job_type: 'run',
|
|
||||||
name: 'Foo',
|
|
||||||
playbook: 'Bar',
|
|
||||||
project: 2,
|
|
||||||
};
|
|
||||||
JobTemplatesAPI.create.mockResolvedValueOnce({
|
JobTemplatesAPI.create.mockResolvedValueOnce({
|
||||||
data: {
|
data: {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -118,7 +123,9 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
context: { router: { history } },
|
context: { router: { history } },
|
||||||
});
|
});
|
||||||
|
|
||||||
await wrapper.find('JobTemplateForm').prop('handleSubmit')(jobTemplateData);
|
await wrapper.find('JobTemplateForm').invoke('handleSubmit')(
|
||||||
|
jobTemplateData
|
||||||
|
);
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
expect(history.push).toHaveBeenCalledWith(
|
expect(history.push).toHaveBeenCalledWith(
|
||||||
'/templates/job_template/1/details'
|
'/templates/job_template/1/details'
|
||||||
@@ -134,7 +141,7 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
context: { router: { history } },
|
context: { router: { history } },
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
||||||
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
|
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
|
||||||
expect(history.push).toHaveBeenCalledWith('/templates');
|
expect(history.push).toHaveBeenCalledWith('/templates');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -102,18 +102,22 @@ class JobTemplateEdit extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleSubmit(values) {
|
async handleSubmit(values) {
|
||||||
|
const { template, history } = this.props;
|
||||||
const {
|
const {
|
||||||
template: { id },
|
newLabels,
|
||||||
history,
|
removedLabels,
|
||||||
} = this.props;
|
addedInstanceGroups,
|
||||||
const { newLabels, removedLabels } = values;
|
removedInstanceGroups,
|
||||||
delete values.newLabels;
|
...remainingValues
|
||||||
delete values.removedLabels;
|
} = values;
|
||||||
|
|
||||||
this.setState({ formSubmitError: null });
|
this.setState({ formSubmitError: null });
|
||||||
try {
|
try {
|
||||||
await JobTemplatesAPI.update(id, values);
|
await JobTemplatesAPI.update(template.id, remainingValues);
|
||||||
await Promise.all([this.submitLabels(newLabels, removedLabels)]);
|
await Promise.all([
|
||||||
|
this.submitLabels(newLabels, removedLabels),
|
||||||
|
this.submitInstanceGroups(addedInstanceGroups, removedInstanceGroups),
|
||||||
|
]);
|
||||||
history.push(this.detailsUrl);
|
history.push(this.detailsUrl);
|
||||||
} catch (formSubmitError) {
|
} catch (formSubmitError) {
|
||||||
this.setState({ formSubmitError });
|
this.setState({ formSubmitError });
|
||||||
@@ -121,18 +125,16 @@ class JobTemplateEdit extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async submitLabels(newLabels = [], removedLabels = []) {
|
async submitLabels(newLabels = [], removedLabels = []) {
|
||||||
const {
|
const { template } = this.props;
|
||||||
template: { id },
|
|
||||||
} = this.props;
|
|
||||||
const disassociationPromises = removedLabels.map(label =>
|
const disassociationPromises = removedLabels.map(label =>
|
||||||
JobTemplatesAPI.disassociateLabel(id, label)
|
JobTemplatesAPI.disassociateLabel(template.id, label)
|
||||||
);
|
);
|
||||||
const associationPromises = newLabels
|
const associationPromises = newLabels
|
||||||
.filter(label => !label.organization)
|
.filter(label => !label.organization)
|
||||||
.map(label => JobTemplatesAPI.associateLabel(id, label));
|
.map(label => JobTemplatesAPI.associateLabel(template.id, label));
|
||||||
const creationPromises = newLabels
|
const creationPromises = newLabels
|
||||||
.filter(label => label.organization)
|
.filter(label => label.organization)
|
||||||
.map(label => JobTemplatesAPI.generateLabel(id, label));
|
.map(label => JobTemplatesAPI.generateLabel(template.id, label));
|
||||||
|
|
||||||
const results = await Promise.all([
|
const results = await Promise.all([
|
||||||
...disassociationPromises,
|
...disassociationPromises,
|
||||||
@@ -142,6 +144,17 @@ class JobTemplateEdit extends Component {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async submitInstanceGroups(addedGroups, removedGroups) {
|
||||||
|
const { template } = this.props;
|
||||||
|
const associatePromises = addedGroups.map(group =>
|
||||||
|
JobTemplatesAPI.associateInstanceGroup(template.id, group.id)
|
||||||
|
);
|
||||||
|
const disassociatePromises = removedGroups.map(group =>
|
||||||
|
JobTemplatesAPI.disassociateInstanceGroup(template.id, group.id)
|
||||||
|
);
|
||||||
|
return Promise.all([...associatePromises, ...disassociatePromises]);
|
||||||
|
}
|
||||||
|
|
||||||
handleCancel() {
|
handleCancel() {
|
||||||
const { history } = this.props;
|
const { history } = this.props;
|
||||||
history.push(this.detailsUrl);
|
history.push(this.detailsUrl);
|
||||||
|
|||||||
@@ -15,6 +15,18 @@ const mockJobTemplate = {
|
|||||||
project: 3,
|
project: 3,
|
||||||
playbook: 'Baz',
|
playbook: 'Baz',
|
||||||
type: 'job_template',
|
type: 'job_template',
|
||||||
|
forks: 0,
|
||||||
|
limit: '',
|
||||||
|
verbosity: '0',
|
||||||
|
job_slice_count: 1,
|
||||||
|
timeout: 0,
|
||||||
|
job_tags: '',
|
||||||
|
skip_tags: '',
|
||||||
|
diff_mode: false,
|
||||||
|
allow_callbacks: false,
|
||||||
|
allow_simultaneous: false,
|
||||||
|
use_fact_cache: false,
|
||||||
|
host_config_key: '',
|
||||||
summary_fields: {
|
summary_fields: {
|
||||||
user_capabilities: {
|
user_capabilities: {
|
||||||
edit: true,
|
edit: true,
|
||||||
@@ -92,6 +104,32 @@ const mockRelatedProjectPlaybooks = [
|
|||||||
'vault.yml',
|
'vault.yml',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const mockInstanceGroups = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: 'instance_group',
|
||||||
|
url: '/api/v2/instance_groups/1/',
|
||||||
|
related: {
|
||||||
|
jobs: '/api/v2/instance_groups/1/jobs/',
|
||||||
|
instances: '/api/v2/instance_groups/1/instances/',
|
||||||
|
},
|
||||||
|
name: 'tower',
|
||||||
|
capacity: 59,
|
||||||
|
committed_capacity: 0,
|
||||||
|
consumed_capacity: 0,
|
||||||
|
percent_capacity_remaining: 100.0,
|
||||||
|
jobs_running: 0,
|
||||||
|
jobs_total: 3,
|
||||||
|
instances: 1,
|
||||||
|
controller: null,
|
||||||
|
is_controller: false,
|
||||||
|
is_isolated: false,
|
||||||
|
policy_instance_percentage: 100,
|
||||||
|
policy_instance_minimum: 0,
|
||||||
|
policy_instance_list: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
JobTemplatesAPI.readCredentials.mockResolvedValue({
|
JobTemplatesAPI.readCredentials.mockResolvedValue({
|
||||||
data: mockRelatedCredentials,
|
data: mockRelatedCredentials,
|
||||||
});
|
});
|
||||||
@@ -101,12 +139,25 @@ ProjectsAPI.readPlaybooks.mockResolvedValue({
|
|||||||
LabelsAPI.read.mockResolvedValue({ data: { results: [] } });
|
LabelsAPI.read.mockResolvedValue({ data: { results: [] } });
|
||||||
|
|
||||||
describe('<JobTemplateEdit />', () => {
|
describe('<JobTemplateEdit />', () => {
|
||||||
test('initially renders successfully', async done => {
|
beforeEach(() => {
|
||||||
|
LabelsAPI.read.mockResolvedValue({ data: { results: [] } });
|
||||||
|
JobTemplatesAPI.readCredentials.mockResolvedValue({
|
||||||
|
data: mockRelatedCredentials,
|
||||||
|
});
|
||||||
|
JobTemplatesAPI.readInstanceGroups.mockReturnValue({
|
||||||
|
data: { results: mockInstanceGroups },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('initially renders successfully', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<JobTemplateEdit template={mockJobTemplate} />
|
<JobTemplateEdit template={mockJobTemplate} />
|
||||||
);
|
);
|
||||||
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleSubmit should call api update', async done => {
|
test('handleSubmit should call api update', async done => {
|
||||||
|
|||||||
@@ -4,30 +4,45 @@ import { withRouter } from 'react-router-dom';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { withFormik, Field } from 'formik';
|
import { withFormik, Field } from 'formik';
|
||||||
import { Form, FormGroup, Tooltip, Card } from '@patternfly/react-core';
|
import {
|
||||||
|
Form,
|
||||||
|
FormGroup,
|
||||||
|
Tooltip,
|
||||||
|
Card,
|
||||||
|
Switch,
|
||||||
|
Checkbox,
|
||||||
|
TextInput,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
|
import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import ContentLoading from '@components/ContentLoading';
|
import ContentLoading from '@components/ContentLoading';
|
||||||
import AnsibleSelect from '@components/AnsibleSelect';
|
import AnsibleSelect from '@components/AnsibleSelect';
|
||||||
import MultiSelect from '@components/MultiSelect';
|
import MultiSelect, { TagMultiSelect } from '@components/MultiSelect';
|
||||||
import FormActionGroup from '@components/FormActionGroup';
|
import FormActionGroup from '@components/FormActionGroup';
|
||||||
import FormField from '@components/FormField';
|
import FormField, { CheckboxField } from '@components/FormField';
|
||||||
import FormRow from '@components/FormRow';
|
import FormRow from '@components/FormRow';
|
||||||
|
import CollapsibleSection from '@components/CollapsibleSection';
|
||||||
import { required } from '@util/validators';
|
import { required } from '@util/validators';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { JobTemplate } from '@types';
|
import { JobTemplate } from '@types';
|
||||||
import InventoriesLookup from './InventoriesLookup';
|
import { InventoriesLookup, InstanceGroupsLookup } from '@components/Lookup';
|
||||||
import ProjectLookup from './ProjectLookup';
|
import ProjectLookup from './ProjectLookup';
|
||||||
import { LabelsAPI, ProjectsAPI } from '@api';
|
import { JobTemplatesAPI, LabelsAPI, ProjectsAPI } from '@api';
|
||||||
|
|
||||||
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
`;
|
`;
|
||||||
const QSConfig = {
|
|
||||||
page: 1,
|
const GridFormGroup = styled(FormGroup)`
|
||||||
page_size: 200,
|
& > label {
|
||||||
order_by: 'name',
|
grid-column: 1 / -1;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
&& {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
class JobTemplateForm extends Component {
|
class JobTemplateForm extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -49,6 +64,7 @@ class JobTemplateForm extends Component {
|
|||||||
labels: { results: [] },
|
labels: { results: [] },
|
||||||
project: null,
|
project: null,
|
||||||
},
|
},
|
||||||
|
isNew: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,31 +79,45 @@ class JobTemplateForm extends Component {
|
|||||||
project: props.template.summary_fields.project,
|
project: props.template.summary_fields.project,
|
||||||
inventory: props.template.summary_fields.inventory,
|
inventory: props.template.summary_fields.inventory,
|
||||||
relatedProjectPlaybooks: props.relatedProjectPlaybooks,
|
relatedProjectPlaybooks: props.relatedProjectPlaybooks,
|
||||||
|
relatedInstanceGroups: [],
|
||||||
|
allowCallbacks: !!props.template.host_config_key,
|
||||||
};
|
};
|
||||||
this.handleNewLabel = this.handleNewLabel.bind(this);
|
this.handleNewLabel = this.handleNewLabel.bind(this);
|
||||||
this.loadLabels = this.loadLabels.bind(this);
|
this.loadLabels = this.loadLabels.bind(this);
|
||||||
this.removeLabel = this.removeLabel.bind(this);
|
this.removeLabel = this.removeLabel.bind(this);
|
||||||
this.handleProjectValidation = this.handleProjectValidation.bind(this);
|
this.handleProjectValidation = this.handleProjectValidation.bind(this);
|
||||||
|
this.loadRelatedInstanceGroups = this.loadRelatedInstanceGroups.bind(this);
|
||||||
this.loadRelatedProjectPlaybooks = this.loadRelatedProjectPlaybooks.bind(
|
this.loadRelatedProjectPlaybooks = this.loadRelatedProjectPlaybooks.bind(
|
||||||
this
|
this
|
||||||
);
|
);
|
||||||
|
this.handleInstanceGroupsChange = this.handleInstanceGroupsChange.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
componentDidMount() {
|
||||||
const { validateField } = this.props;
|
const { validateField } = this.props;
|
||||||
await this.loadLabels(QSConfig);
|
this.setState({ contentError: null, hasContentLoading: true });
|
||||||
validateField('project');
|
Promise.all([this.loadLabels(), this.loadRelatedInstanceGroups()]).then(
|
||||||
|
() => {
|
||||||
|
this.setState({ hasContentLoading: false });
|
||||||
|
validateField('project');
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadLabels(QueryConfig) {
|
async loadLabels() {
|
||||||
// This function assumes that the user has no more than 400
|
// This function assumes that the user has no more than 400
|
||||||
// labels. For the vast majority of users this will be more thans
|
// labels. For the vast majority of users this will be more thans
|
||||||
// enough.This can be updated to allow more than 400 labels if we
|
// enough. This can be updated to allow more than 400 labels if we
|
||||||
// decide it is necessary.
|
// decide it is necessary.
|
||||||
this.setState({ contentError: null, hasContentLoading: true });
|
|
||||||
let loadedLabels;
|
let loadedLabels;
|
||||||
try {
|
try {
|
||||||
const { data } = await LabelsAPI.read(QueryConfig);
|
const { data } = await LabelsAPI.read({
|
||||||
|
page: 1,
|
||||||
|
page_size: 200,
|
||||||
|
order_by: 'name',
|
||||||
|
});
|
||||||
loadedLabels = [...data.results];
|
loadedLabels = [...data.results];
|
||||||
if (data.next && data.next.includes('page=2')) {
|
if (data.next && data.next.includes('page=2')) {
|
||||||
const {
|
const {
|
||||||
@@ -102,8 +132,22 @@ class JobTemplateForm extends Component {
|
|||||||
this.setState({ loadedLabels });
|
this.setState({ loadedLabels });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ contentError: err });
|
this.setState({ contentError: err });
|
||||||
} finally {
|
}
|
||||||
this.setState({ hasContentLoading: false });
|
}
|
||||||
|
|
||||||
|
async loadRelatedInstanceGroups() {
|
||||||
|
const { template } = this.props;
|
||||||
|
if (!template.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { data } = await JobTemplatesAPI.readInstanceGroups(template.id);
|
||||||
|
this.setState({
|
||||||
|
initialInstanceGroups: data.results,
|
||||||
|
relatedInstanceGroups: [...data.results],
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.setState({ contentError: err });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,23 +160,6 @@ class JobTemplateForm extends Component {
|
|||||||
newLabel => newLabel.name !== label
|
newLabel => newLabel.name !== label
|
||||||
);
|
);
|
||||||
this.setState({ newLabels: filteredLabels });
|
this.setState({ newLabels: filteredLabels });
|
||||||
} else if (typeof label === 'string') {
|
|
||||||
setFieldValue('newLabels', [
|
|
||||||
...newLabels,
|
|
||||||
{
|
|
||||||
name: label,
|
|
||||||
organization: template.summary_fields.inventory.organization_id,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
this.setState({
|
|
||||||
newLabels: [
|
|
||||||
...newLabels,
|
|
||||||
{
|
|
||||||
name: label,
|
|
||||||
organization: template.summary_fields.inventory.organization_id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
setFieldValue('newLabels', [
|
setFieldValue('newLabels', [
|
||||||
...newLabels,
|
...newLabels,
|
||||||
@@ -141,7 +168,12 @@ class JobTemplateForm extends Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
newLabels: [
|
newLabels: [
|
||||||
...newLabels,
|
...newLabels,
|
||||||
{ name: label.name, associate: true, id: label.id },
|
{
|
||||||
|
name: label.name,
|
||||||
|
associate: true,
|
||||||
|
id: label.id,
|
||||||
|
organization: template.summary_fields.inventory.organization_id,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -201,6 +233,30 @@ class JobTemplateForm extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleInstanceGroupsChange(relatedInstanceGroups) {
|
||||||
|
const { setFieldValue } = this.props;
|
||||||
|
const { initialInstanceGroups } = this.state;
|
||||||
|
let added = [];
|
||||||
|
const removed = [];
|
||||||
|
if (initialInstanceGroups) {
|
||||||
|
initialInstanceGroups.forEach(group => {
|
||||||
|
if (!relatedInstanceGroups.find(g => g.id === group.id)) {
|
||||||
|
removed.push(group);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
relatedInstanceGroups.forEach(group => {
|
||||||
|
if (!initialInstanceGroups.find(g => g.id === group.id)) {
|
||||||
|
added.push(group);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
added = relatedInstanceGroups;
|
||||||
|
}
|
||||||
|
setFieldValue('addedInstanceGroups', added);
|
||||||
|
setFieldValue('removedInstanceGroups', removed);
|
||||||
|
this.setState({ relatedInstanceGroups });
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
loadedLabels,
|
loadedLabels,
|
||||||
@@ -209,6 +265,8 @@ class JobTemplateForm extends Component {
|
|||||||
inventory,
|
inventory,
|
||||||
project,
|
project,
|
||||||
relatedProjectPlaybooks = [],
|
relatedProjectPlaybooks = [],
|
||||||
|
relatedInstanceGroups,
|
||||||
|
allowCallbacks,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const {
|
const {
|
||||||
handleCancel,
|
handleCancel,
|
||||||
@@ -255,6 +313,20 @@ class JobTemplateForm extends Component {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const verbosityOptions = [
|
||||||
|
{ value: '0', key: '0', label: i18n._(t`0 (Normal)`) },
|
||||||
|
{ value: '1', key: '1', label: i18n._(t`1 (Verbose)`) },
|
||||||
|
{ value: '2', key: '2', label: i18n._(t`2 (More Verbose)`) },
|
||||||
|
{ value: '3', key: '3', label: i18n._(t`3 (Debug)`) },
|
||||||
|
{ value: '4', key: '4', label: i18n._(t`4 (Connection Debug)`) },
|
||||||
|
];
|
||||||
|
let callbackUrl;
|
||||||
|
if (template && template.related) {
|
||||||
|
const { origin } = document.location;
|
||||||
|
const path = template.related.callback || `${template.url}callback`;
|
||||||
|
callbackUrl = `${origin}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (hasContentLoading) {
|
if (hasContentLoading) {
|
||||||
return (
|
return (
|
||||||
<Card className="awx-c-card">
|
<Card className="awx-c-card">
|
||||||
@@ -270,7 +342,7 @@ class JobTemplateForm extends Component {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const AdvancedFieldsWrapper = template.isNew ? CollapsibleSection : 'div';
|
||||||
return (
|
return (
|
||||||
<Form autoComplete="off" onSubmit={handleSubmit}>
|
<Form autoComplete="off" onSubmit={handleSubmit}>
|
||||||
<FormRow>
|
<FormRow>
|
||||||
@@ -293,8 +365,7 @@ class JobTemplateForm extends Component {
|
|||||||
validate={required(null, i18n)}
|
validate={required(null, i18n)}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
render={({ form, field }) => {
|
render={({ form, field }) => {
|
||||||
const isValid =
|
const isValid = !form.touched.job_type || !form.errors.job_type;
|
||||||
form && (!form.touched[field.name] || !form.errors[field.name]);
|
|
||||||
return (
|
return (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
fieldId="template-job-type"
|
fieldId="template-job-type"
|
||||||
@@ -314,7 +385,7 @@ class JobTemplateForm extends Component {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
id="job_type"
|
id="template-job-type"
|
||||||
data={jobTypeOptions}
|
data={jobTypeOptions}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@@ -341,34 +412,29 @@ class JobTemplateForm extends Component {
|
|||||||
<Field
|
<Field
|
||||||
name="project"
|
name="project"
|
||||||
validate={this.handleProjectValidation()}
|
validate={this.handleProjectValidation()}
|
||||||
render={({ form }) => {
|
render={({ form }) => (
|
||||||
const isValid = form && !form.errors.project;
|
<ProjectLookup
|
||||||
return (
|
helperTextInvalid={form.errors.project}
|
||||||
<ProjectLookup
|
isValid={!form.errors.project}
|
||||||
helperTextInvalid={form.errors.project}
|
value={project}
|
||||||
isValid={isValid}
|
onBlur={handleBlur}
|
||||||
value={project}
|
tooltip={i18n._(t`Select the project containing the playbook
|
||||||
onBlur={handleBlur}
|
|
||||||
tooltip={i18n._(t`Select the project containing the playbook
|
|
||||||
you want this job to execute.`)}
|
you want this job to execute.`)}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
this.loadRelatedProjectPlaybooks(value.id);
|
this.loadRelatedProjectPlaybooks(value.id);
|
||||||
form.setFieldValue('project', value.id);
|
form.setFieldValue('project', value.id);
|
||||||
form.setFieldTouched('project');
|
this.setState({ project: value });
|
||||||
this.setState({ project: value });
|
}}
|
||||||
}}
|
required
|
||||||
required
|
/>
|
||||||
/>
|
)}
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Field
|
<Field
|
||||||
name="playbook"
|
name="playbook"
|
||||||
validate={required(i18n._(t`Select a value for this field`), i18n)}
|
validate={required(i18n._(t`Select a value for this field`), i18n)}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
render={({ field, form }) => {
|
render={({ field, form }) => {
|
||||||
const isValid =
|
const isValid = !form.touched.playbook || !form.errors.playbook;
|
||||||
form && (!form.touched[field.name] || !form.errors[field.name]);
|
|
||||||
return (
|
return (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
fieldId="template-playbook"
|
fieldId="template-playbook"
|
||||||
@@ -386,7 +452,7 @@ class JobTemplateForm extends Component {
|
|||||||
<QuestionCircleIcon />
|
<QuestionCircleIcon />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
id="playbook"
|
id="template-playbook"
|
||||||
data={playbookOptions}
|
data={playbookOptions}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
form={form}
|
form={form}
|
||||||
@@ -415,6 +481,251 @@ class JobTemplateForm extends Component {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
|
<AdvancedFieldsWrapper label="Advanced">
|
||||||
|
<FormRow>
|
||||||
|
<FormField
|
||||||
|
id="template-forks"
|
||||||
|
name="forks"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
label={i18n._(t`Forks`)}
|
||||||
|
tooltip={
|
||||||
|
<span>
|
||||||
|
{i18n._(t`The number of parallel or simultaneous
|
||||||
|
processes to use while executing the playbook. An empty value,
|
||||||
|
or a value less than 1 will use the Ansible default which is
|
||||||
|
usually 5. The default number of forks can be overwritten
|
||||||
|
with a change to`)}{' '}
|
||||||
|
<code>ansible.cfg</code>.{' '}
|
||||||
|
{i18n._(t`Refer to the Ansible documentation for details
|
||||||
|
about the configuration file.`)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="template-limit"
|
||||||
|
name="limit"
|
||||||
|
type="text"
|
||||||
|
label={i18n._(t`Limit`)}
|
||||||
|
tooltip={i18n._(t`Provide a host pattern to further constrain
|
||||||
|
the list of hosts that will be managed or affected by the
|
||||||
|
playbook. Multiple patterns are allowed. Refer to Ansible
|
||||||
|
documentation for more information and examples on patterns.`)}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
name="verbosity"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormGroup
|
||||||
|
fieldId="template-verbosity"
|
||||||
|
label={i18n._(t`Verbosity`)}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
position="right"
|
||||||
|
content={i18n._(t`Control the level of output ansible will
|
||||||
|
produce as the playbook executes.`)}
|
||||||
|
>
|
||||||
|
<QuestionCircleIcon />
|
||||||
|
</Tooltip>
|
||||||
|
<AnsibleSelect
|
||||||
|
id="template-verbosity"
|
||||||
|
data={verbosityOptions}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="template-job-slicing"
|
||||||
|
name="job_slice_count"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
label={i18n._(t`Job Slicing`)}
|
||||||
|
tooltip={i18n._(t`Divide the work done by this job template
|
||||||
|
into the specified number of job slices, each running the
|
||||||
|
same tasks against a portion of the inventory.`)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="template-timeout"
|
||||||
|
name="timeout"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
label={i18n._(t`Timeout`)}
|
||||||
|
tooltip={i18n._(t`The amount of time (in seconds) to run
|
||||||
|
before the task is canceled. Defaults to 0 for no job
|
||||||
|
timeout.`)}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
name="diff_mode"
|
||||||
|
render={({ field, form }) => (
|
||||||
|
<FormGroup
|
||||||
|
fieldId="template-show-changes"
|
||||||
|
label={i18n._(t`Show Changes`)}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
position="right"
|
||||||
|
content={i18n._(t`If enabled, show the changes made by
|
||||||
|
Ansible tasks, where supported. This is equivalent
|
||||||
|
to Ansible’s --diff mode.`)}
|
||||||
|
>
|
||||||
|
<QuestionCircleIcon />
|
||||||
|
</Tooltip>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
id="template-show-changes"
|
||||||
|
label={field.value ? i18n._(t`On`) : i18n._(t`Off`)}
|
||||||
|
isChecked={field.value}
|
||||||
|
onChange={checked =>
|
||||||
|
form.setFieldValue(field.name, checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormRow>
|
||||||
|
<InstanceGroupsLookup
|
||||||
|
css="margin-top: 20px"
|
||||||
|
value={relatedInstanceGroups}
|
||||||
|
onChange={this.handleInstanceGroupsChange}
|
||||||
|
tooltip={i18n._(
|
||||||
|
t`Select the Instance Groups for this Organization to run on.`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
name="job_tags"
|
||||||
|
render={({ field, form }) => (
|
||||||
|
<FormGroup
|
||||||
|
label={i18n._(t`Job Tags`)}
|
||||||
|
css="margin-top: 20px"
|
||||||
|
fieldId="template-job-tags"
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
position="right"
|
||||||
|
content={i18n._(t`Tags are useful when you have a large
|
||||||
|
playbook, and you want to run a specific part of a
|
||||||
|
play or task. Use commas to separate multiple tags.
|
||||||
|
Refer to Ansible Tower documentation for details on
|
||||||
|
the usage of tags.`)}
|
||||||
|
>
|
||||||
|
<QuestionCircleIcon />
|
||||||
|
</Tooltip>
|
||||||
|
<TagMultiSelect
|
||||||
|
value={field.value}
|
||||||
|
onChange={value => form.setFieldValue(field.name, value)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
name="skip_tags"
|
||||||
|
render={({ field, form }) => (
|
||||||
|
<FormGroup
|
||||||
|
label={i18n._(t`Skip Tags`)}
|
||||||
|
css="margin-top: 20px"
|
||||||
|
fieldId="template-skip-tags"
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
position="right"
|
||||||
|
content={i18n._(t`Skip tags are useful when you have a
|
||||||
|
large playbook, and you want to skip specific parts of a
|
||||||
|
play or task. Use commas to separate multiple tags. Refer
|
||||||
|
to Ansible Tower documentation for details on the usage
|
||||||
|
of tags.`)}
|
||||||
|
>
|
||||||
|
<QuestionCircleIcon />
|
||||||
|
</Tooltip>
|
||||||
|
<TagMultiSelect
|
||||||
|
value={field.value}
|
||||||
|
onChange={value => form.setFieldValue(field.name, value)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<GridFormGroup
|
||||||
|
fieldId="template-option-checkboxes"
|
||||||
|
isInline
|
||||||
|
label={i18n._(t`Options`)}
|
||||||
|
css="margin-top: 20px"
|
||||||
|
>
|
||||||
|
<CheckboxField
|
||||||
|
id="option-privilege-escalation"
|
||||||
|
name="become_enabled"
|
||||||
|
label={i18n._(t`Privilege Escalation`)}
|
||||||
|
tooltip={i18n._(
|
||||||
|
t`If enabled, run this playbook as an administrator.`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
aria-label={i18n._(t`Provisioning Callbacks`)}
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
{i18n._(t`Provisioning Callbacks`)}
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
position="right"
|
||||||
|
content={i18n._(
|
||||||
|
t`Enables creation of a provisioning callback URL. Using
|
||||||
|
the URL a host can contact BRAND_NAME and request a
|
||||||
|
configuration update using this job template.`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<QuestionCircleIcon />
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
id="option-callbacks"
|
||||||
|
isChecked={allowCallbacks}
|
||||||
|
onChange={checked => {
|
||||||
|
this.setState({ allowCallbacks: checked });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CheckboxField
|
||||||
|
id="option-concurrent"
|
||||||
|
name="allow_simultaneous"
|
||||||
|
label={i18n._(t`Concurrent Jobs`)}
|
||||||
|
tooltip={i18n._(
|
||||||
|
t`If enabled, simultaneous runs of this job template will
|
||||||
|
be allowed.`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<CheckboxField
|
||||||
|
id="option-fact-cache"
|
||||||
|
name="use_fact_cache"
|
||||||
|
label={i18n._(t`Fact Cache`)}
|
||||||
|
tooltip={i18n._(
|
||||||
|
t`If enabled, use cached facts if available and store
|
||||||
|
discovered facts in the cache.`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</GridFormGroup>
|
||||||
|
<div
|
||||||
|
css={`
|
||||||
|
${allowCallbacks ? '' : 'display: none'}
|
||||||
|
margin-top: 20px;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<FormRow>
|
||||||
|
{callbackUrl && (
|
||||||
|
<FormGroup
|
||||||
|
label={i18n._(t`Provisioning Callback URL`)}
|
||||||
|
fieldId="template-callback-url"
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="template-callback-url"
|
||||||
|
isDisabled
|
||||||
|
value={callbackUrl}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
<FormField
|
||||||
|
id="template-host-config-key"
|
||||||
|
name="host_config_key"
|
||||||
|
label={i18n._(t`Host Config Key`)}
|
||||||
|
validate={allowCallbacks ? required(null, i18n) : null}
|
||||||
|
/>
|
||||||
|
</FormRow>
|
||||||
|
</div>
|
||||||
|
</AdvancedFieldsWrapper>
|
||||||
<FormActionGroup onCancel={handleCancel} onSubmit={handleSubmit} />
|
<FormActionGroup onCancel={handleCancel} onSubmit={handleSubmit} />
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
@@ -429,8 +740,21 @@ const FormikApp = withFormik({
|
|||||||
description = '',
|
description = '',
|
||||||
job_type = 'run',
|
job_type = 'run',
|
||||||
inventory = '',
|
inventory = '',
|
||||||
playbook = '',
|
|
||||||
project = '',
|
project = '',
|
||||||
|
playbook = '',
|
||||||
|
forks,
|
||||||
|
limit,
|
||||||
|
verbosity,
|
||||||
|
job_slice_count,
|
||||||
|
timeout,
|
||||||
|
diff_mode,
|
||||||
|
job_tags,
|
||||||
|
skip_tags,
|
||||||
|
become_enabled,
|
||||||
|
allow_callbacks,
|
||||||
|
allow_simultaneous,
|
||||||
|
use_fact_cache,
|
||||||
|
host_config_key,
|
||||||
summary_fields = { labels: { results: [] } },
|
summary_fields = { labels: { results: [] } },
|
||||||
} = { ...template };
|
} = { ...template };
|
||||||
|
|
||||||
@@ -442,6 +766,19 @@ const FormikApp = withFormik({
|
|||||||
project: project || '',
|
project: project || '',
|
||||||
playbook: playbook || '',
|
playbook: playbook || '',
|
||||||
labels: summary_fields.labels.results,
|
labels: summary_fields.labels.results,
|
||||||
|
forks: forks || 0,
|
||||||
|
limit: limit || '',
|
||||||
|
verbosity: verbosity || '0',
|
||||||
|
job_slice_count: job_slice_count || 1,
|
||||||
|
timeout: timeout || 0,
|
||||||
|
diff_mode: diff_mode || false,
|
||||||
|
job_tags: job_tags || '',
|
||||||
|
skip_tags: skip_tags || '',
|
||||||
|
become_enabled: become_enabled || false,
|
||||||
|
allow_callbacks: allow_callbacks || false,
|
||||||
|
allow_simultaneous: allow_simultaneous || false,
|
||||||
|
use_fact_cache: use_fact_cache || false,
|
||||||
|
host_config_key: host_config_key || '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleSubmit: (values, bag) => bag.props.handleSubmit(values),
|
handleSubmit: (values, bag) => bag.props.handleSubmit(values),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||||
import { sleep } from '@testUtils/testUtils';
|
import { sleep } from '@testUtils/testUtils';
|
||||||
import JobTemplateForm, { _JobTemplateForm } from './JobTemplateForm';
|
import JobTemplateForm, { _JobTemplateForm } from './JobTemplateForm';
|
||||||
import { LabelsAPI } from '@api';
|
import { LabelsAPI, JobTemplatesAPI } from '@api';
|
||||||
|
|
||||||
jest.mock('@api');
|
jest.mock('@api');
|
||||||
|
|
||||||
@@ -29,17 +29,45 @@ describe('<JobTemplateForm />', () => {
|
|||||||
labels: { results: [{ name: 'Sushi', id: 1 }, { name: 'Major', id: 2 }] },
|
labels: { results: [{ name: 'Sushi', id: 1 }, { name: 'Major', id: 2 }] },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const mockInstanceGroups = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: 'instance_group',
|
||||||
|
url: '/api/v2/instance_groups/1/',
|
||||||
|
related: {
|
||||||
|
jobs: '/api/v2/instance_groups/1/jobs/',
|
||||||
|
instances: '/api/v2/instance_groups/1/instances/',
|
||||||
|
},
|
||||||
|
name: 'tower',
|
||||||
|
capacity: 59,
|
||||||
|
committed_capacity: 0,
|
||||||
|
consumed_capacity: 0,
|
||||||
|
percent_capacity_remaining: 100.0,
|
||||||
|
jobs_running: 0,
|
||||||
|
jobs_total: 3,
|
||||||
|
instances: 1,
|
||||||
|
controller: null,
|
||||||
|
is_controller: false,
|
||||||
|
is_isolated: false,
|
||||||
|
policy_instance_percentage: 100,
|
||||||
|
policy_instance_minimum: 0,
|
||||||
|
policy_instance_list: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
LabelsAPI.read.mockReturnValue({
|
LabelsAPI.read.mockReturnValue({
|
||||||
data: mockData.summary_fields.labels,
|
data: mockData.summary_fields.labels,
|
||||||
});
|
});
|
||||||
|
JobTemplatesAPI.readInstanceGroups.mockReturnValue({
|
||||||
|
data: { results: mockInstanceGroups },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders successfully', async done => {
|
test('should render labels MultiSelect', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<JobTemplateForm
|
<JobTemplateForm
|
||||||
template={mockData}
|
template={mockData}
|
||||||
@@ -47,19 +75,18 @@ describe('<JobTemplateForm />', () => {
|
|||||||
handleCancel={jest.fn()}
|
handleCancel={jest.fn()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
await waitForElement(wrapper, 'Form', el => el.length === 0);
|
||||||
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
|
||||||
expect(LabelsAPI.read).toHaveBeenCalled();
|
expect(LabelsAPI.read).toHaveBeenCalled();
|
||||||
|
expect(JobTemplatesAPI.readInstanceGroups).toHaveBeenCalled();
|
||||||
|
wrapper.update();
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('FormGroup[fieldId="template-labels"] MultiSelect Chip')
|
.find('FormGroup[fieldId="template-labels"] MultiSelect')
|
||||||
.first()
|
.prop('associatedItems')
|
||||||
.text()
|
).toEqual(mockData.summary_fields.labels.results);
|
||||||
).toEqual('Sushi');
|
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should update form values on input changes', async done => {
|
test('should update form values on input changes', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<JobTemplateForm
|
<JobTemplateForm
|
||||||
template={mockData}
|
template={mockData}
|
||||||
@@ -96,10 +123,9 @@ describe('<JobTemplateForm />', () => {
|
|||||||
target: { value: 'new baz type', name: 'playbook' },
|
target: { value: 'new baz type', name: 'playbook' },
|
||||||
});
|
});
|
||||||
expect(form.state('values').playbook).toEqual('new baz type');
|
expect(form.state('values').playbook).toEqual('new baz type');
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should call handleSubmit when Submit button is clicked', async done => {
|
test('should call handleSubmit when Submit button is clicked', async () => {
|
||||||
const handleSubmit = jest.fn();
|
const handleSubmit = jest.fn();
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<JobTemplateForm
|
<JobTemplateForm
|
||||||
@@ -113,10 +139,9 @@ describe('<JobTemplateForm />', () => {
|
|||||||
wrapper.find('button[aria-label="Save"]').simulate('click');
|
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||||
await sleep(1);
|
await sleep(1);
|
||||||
expect(handleSubmit).toBeCalled();
|
expect(handleSubmit).toBeCalled();
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should call handleCancel when Cancel button is clicked', async done => {
|
test('should call handleCancel when Cancel button is clicked', async () => {
|
||||||
const handleCancel = jest.fn();
|
const handleCancel = jest.fn();
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<JobTemplateForm
|
<JobTemplateForm
|
||||||
@@ -129,10 +154,9 @@ describe('<JobTemplateForm />', () => {
|
|||||||
expect(handleCancel).not.toHaveBeenCalled();
|
expect(handleCancel).not.toHaveBeenCalled();
|
||||||
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
|
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
|
||||||
expect(handleCancel).toBeCalled();
|
expect(handleCancel).toBeCalled();
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should call loadRelatedProjectPlaybooks when project value changes', async done => {
|
test('should call loadRelatedProjectPlaybooks when project value changes', async () => {
|
||||||
const loadRelatedProjectPlaybooks = jest.spyOn(
|
const loadRelatedProjectPlaybooks = jest.spyOn(
|
||||||
_JobTemplateForm.prototype,
|
_JobTemplateForm.prototype,
|
||||||
'loadRelatedProjectPlaybooks'
|
'loadRelatedProjectPlaybooks'
|
||||||
@@ -150,14 +174,9 @@ describe('<JobTemplateForm />', () => {
|
|||||||
name: 'project',
|
name: 'project',
|
||||||
});
|
});
|
||||||
expect(loadRelatedProjectPlaybooks).toHaveBeenCalledWith(10);
|
expect(loadRelatedProjectPlaybooks).toHaveBeenCalledWith(10);
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleNewLabel should arrange new labels properly', async done => {
|
test('handleNewLabel should arrange new labels properly', async () => {
|
||||||
const handleNewLabel = jest.spyOn(
|
|
||||||
_JobTemplateForm.prototype,
|
|
||||||
'handleNewLabel'
|
|
||||||
);
|
|
||||||
const event = { key: 'Enter', preventDefault: () => {} };
|
const event = { key: 'Enter', preventDefault: () => {} };
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<JobTemplateForm
|
<JobTemplateForm
|
||||||
@@ -167,22 +186,25 @@ describe('<JobTemplateForm />', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
||||||
const multiSelect = wrapper.find('MultiSelect');
|
const multiSelect = wrapper.find(
|
||||||
|
'FormGroup[fieldId="template-labels"] MultiSelect'
|
||||||
|
);
|
||||||
const component = wrapper.find('JobTemplateForm');
|
const component = wrapper.find('JobTemplateForm');
|
||||||
|
|
||||||
wrapper.setState({ newLabels: [], loadedLabels: [], removedLabels: [] });
|
wrapper.setState({ newLabels: [], loadedLabels: [], removedLabels: [] });
|
||||||
multiSelect.setState({ input: 'Foo' });
|
multiSelect.setState({ input: 'Foo' });
|
||||||
component.find('input[aria-label="labels"]').prop('onKeyDown')(event);
|
component
|
||||||
expect(handleNewLabel).toHaveBeenCalledWith('Foo');
|
.find('FormGroup[fieldId="template-labels"] input[aria-label="labels"]')
|
||||||
|
.prop('onKeyDown')(event);
|
||||||
|
|
||||||
component.instance().handleNewLabel({ name: 'Bar', id: 2 });
|
component.instance().handleNewLabel({ name: 'Bar', id: 2 });
|
||||||
expect(component.state().newLabels).toEqual([
|
const newLabels = component.state('newLabels');
|
||||||
{ name: 'Foo', organization: 1 },
|
expect(newLabels).toHaveLength(2);
|
||||||
{ associate: true, id: 2, name: 'Bar' },
|
expect(newLabels[0].name).toEqual('Foo');
|
||||||
]);
|
expect(newLabels[0].organization).toEqual(1);
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
test('disassociateLabel should arrange new labels properly', async done => {
|
|
||||||
|
test('disassociateLabel should arrange new labels properly', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<JobTemplateForm
|
<JobTemplateForm
|
||||||
template={mockData}
|
template={mockData}
|
||||||
@@ -203,6 +225,5 @@ describe('<JobTemplateForm />', () => {
|
|||||||
component.instance().removeLabel({ name: 'Sushi', id: 1 });
|
component.instance().removeLabel({ name: 'Sushi', id: 1 });
|
||||||
expect(component.state().newLabels.length).toBe(0);
|
expect(component.state().newLabels.length).toBe(0);
|
||||||
expect(component.state().removedLabels.length).toBe(1);
|
expect(component.state().removedLabels.length).toBe(1);
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
16
awx/ui_next/src/util/omitProps.jsx
Normal file
16
awx/ui_next/src/util/omitProps.jsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Prevents styled-components from passing down an unsupported
|
||||||
|
* props to children, resulting in console warnings.
|
||||||
|
* https://github.com/styled-components/styled-components/issues/439
|
||||||
|
*/
|
||||||
|
export default function omitProps(Component, ...omit) {
|
||||||
|
return function Omit(props) {
|
||||||
|
const clean = { ...props };
|
||||||
|
omit.forEach(key => {
|
||||||
|
delete clean[key];
|
||||||
|
});
|
||||||
|
return <Component {...clean} />;
|
||||||
|
};
|
||||||
|
}
|
||||||
35
awx/ui_next/src/util/omitProps.test.jsx
Normal file
35
awx/ui_next/src/util/omitProps.test.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
import omitProps from './omitProps';
|
||||||
|
|
||||||
|
describe('omitProps', () => {
|
||||||
|
test('should render child component', () => {
|
||||||
|
const Omit = omitProps('div');
|
||||||
|
const wrapper = mount(<Omit foo="one" bar="two" />);
|
||||||
|
|
||||||
|
const div = wrapper.find('div');
|
||||||
|
expect(div).toHaveLength(1);
|
||||||
|
expect(div.prop('foo')).toEqual('one');
|
||||||
|
expect(div.prop('bar')).toEqual('two');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not pass omitted props to child component', () => {
|
||||||
|
const Omit = omitProps('div', 'foo', 'bar');
|
||||||
|
const wrapper = mount(<Omit foo="one" bar="two" />);
|
||||||
|
|
||||||
|
const div = wrapper.find('div');
|
||||||
|
expect(div).toHaveLength(1);
|
||||||
|
expect(div.prop('foo')).toEqual(undefined);
|
||||||
|
expect(div.prop('bar')).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should support mix of omitted and non-omitted props', () => {
|
||||||
|
const Omit = omitProps('div', 'foo');
|
||||||
|
const wrapper = mount(<Omit foo="one" bar="two" />);
|
||||||
|
|
||||||
|
const div = wrapper.find('div');
|
||||||
|
expect(div).toHaveLength(1);
|
||||||
|
expect(div.prop('foo')).toEqual(undefined);
|
||||||
|
expect(div.prop('bar')).toEqual('two');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user