Feat/bootstrap (#281)
* feat: add bootstrap endpoint redux integration * fix: remove useEffect from app * feat: add path provider * feat: browser router * fix: delete path formatter * fix: return absolute path if no basepath * fix: format seenURI * feat: get bootstrap uri from html * fix: remove unused imports * fix: remove initial loading call * fix: wrap logout in formatApiPath * feat: import logo * feat: remove accessor from receiveConfig * fix: update tests * fix: update asset paths * fix: remove data from app * fix: revert moving access provider * fix: remove build watch * fix: remove console logs * fix: update asset paths * fix: remove path logic from base64 * fix: remove unused import * set uiconfig * change notification text * fix: match uiConfig with expected format * feat: add proclamation * fix: move proclamation * fix: remove unused imports * fix: add target _blank * fix: allow optional toast * fix: return empty string if default value is present * fix: set basepath to empty string if it matches default
@ -1,17 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="unleash">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="baseUriPath" content="::baseUriPath::" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="unleash" />
|
||||
|
||||
<title>Unleash - Enterprise ready feature toggles</title>
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id='app'></div>
|
||||
</body>
|
||||
<title>Unleash - Enterprise ready feature toggles</title>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
1
frontend/src/assets/icons/datadog.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="2500" viewBox=".27 .27 800.01 858.98" width="2328" xmlns="http://www.w3.org/2000/svg"><path d="m670.38 608.27-71.24-46.99-59.43 99.27-69.12-20.21-60.86 92.89 3.12 29.24 330.9-60.97-19.22-206.75zm-308.59-89.14 53.09-7.3c8.59 3.86 14.57 5.33 24.87 7.95 16.04 4.18 34.61 8.19 62.11-5.67 6.4-3.17 19.73-15.36 25.12-22.31l217.52-39.46 22.19 268.56-372.65 67.16zm404.06-96.77-21.47 4.09-41.25-426.18-702.86 81.5 86.59 702.68 82.27-11.94c-6.57-9.38-16.8-20.73-34.27-35.26-24.23-20.13-15.66-54.32-1.37-75.91 18.91-36.48 116.34-82.84 110.82-141.15-1.98-21.2-5.35-48.8-25.03-67.71-.74 7.85.59 15.41.59 15.41s-8.08-10.31-12.11-24.37c-4-5.39-7.14-7.11-11.39-14.31-3.03 8.33-2.63 17.99-2.63 17.99s-6.61-15.62-7.68-28.8c-3.92 5.9-4.91 17.11-4.91 17.11s-8.59-24.62-6.63-37.88c-3.92-11.54-15.54-34.44-12.25-86.49 21.45 15.03 68.67 11.46 87.07-15.66 6.11-8.98 10.29-33.5-3.05-81.81-8.57-30.98-29.79-77.11-38.06-94.61l-.99.71c4.36 14.1 13.35 43.66 16.8 57.99 10.44 43.47 13.24 58.6 8.34 78.64-4.17 17.42-14.17 28.82-39.52 41.56-25.35 12.78-58.99-18.32-61.12-20.04-24.63-19.62-43.68-51.63-45.81-67.18-2.21-17.02 9.81-27.24 15.87-41.16-8.67 2.48-18.34 6.88-18.34 6.88s11.54-11.94 25.77-22.27c5.89-3.9 9.35-6.38 15.56-11.54-8.99-.15-16.29.11-16.29.11s14.99-8.1 30.53-14c-11.37-.5-22.25-.08-22.25-.08s33.45-14.96 59.87-25.94c18.17-7.45 35.92-5.25 45.89 9.17 13.09 18.89 26.84 29.15 55.98 35.51 17.89-7.93 23.33-12.01 45.81-18.13 19.79-21.76 35.33-24.58 35.33-24.58s-7.71 7.07-9.77 18.18c11.22-8.84 23.52-16.22 23.52-16.22s-4.76 5.88-9.2 15.22l1.03 1.53c13.09-7.85 28.48-14.04 28.48-14.04s-4.4 5.56-9.56 12.76c9.87-.08 29.89.42 37.66 1.3 45.87 1.01 55.39-48.99 72.99-55.26 22.04-7.87 31.89-12.63 69.45 24.26 32.23 31.67 57.41 88.36 44.91 101.06-10.48 10.54-31.16-4.11-54.08-32.68-12.11-15.13-21.27-33.01-25.56-55.74-3.62-19.18-17.71-30.31-17.71-30.31s8.18 18.18 8.18 34.24c0 8.77 1.1 41.56 15.16 59.96-1.39 2.69-2.04 13.31-3.58 15.34-16.36-19.77-51.49-33.92-57.22-38.09 19.39 15.89 63.96 52.39 81.08 87.37 16.19 33.08 6.65 63.4 14.84 71.25 2.33 2.25 34.82 42.73 41.07 63.07 10.9 35.45.65 72.7-13.62 95.81l-39.85 6.21c-5.83-1.62-9.76-2.43-14.99-5.46 2.88-5.1 8.61-17.82 8.67-20.44l-2.25-3.95c-12.4 17.57-33.18 34.63-50.44 44.43-22.59 12.8-48.63 10.83-65.58 5.58-48.11-14.84-93.6-47.35-104.57-55.89 0 0-.34 6.82 1.73 8.35 12.13 13.68 39.92 38.43 66.78 55.68l-57.26 6.3 27.07 210.78c-12 1.72-13.87 2.56-27.01 4.43-11.58-40.91-33.73-67.62-57.94-83.18-21.35-13.72-50.8-16.81-78.99-11.23l-1.81 2.1c19.6-2.04 42.74.8 66.51 15.85 23.33 14.75 42.13 52.85 49.05 75.79 8.86 29.32 14.99 60.68-8.86 93.92-16.97 23.63-66.51 36.69-106.53 8.44 10.69 17.19 25.14 31.25 44.59 33.9 28.88 3.92 56.29-1.09 75.16-20.46 16.11-16.56 24.65-51.19 22.4-87.66l25.49-3.7 9.2 65.46 421.98-50.81zm-256.73-177.77c-1.18 2.69-3.03 4.45-.25 13.2l.17.5.44 1.13 1.16 2.62c5.01 10.24 10.51 19.9 19.7 24.83 2.38-.4 4.84-.67 7.39-.8 8.63-.38 14.08.99 17.54 2.85.31-1.72.38-4.24.19-7.95-.67-12.97 2.57-35.03-22.36-46.64-9.41-4.37-22.61-3.02-27.01 2.43.8.1 1.52.27 2.08.46 6.65 2.33 2.14 4.62.95 7.37m69.87 121.02c-3.27-1.8-18.55-1.09-29.29.19-20.46 2.41-42.55 9.51-47.39 13.29-8.8 6.8-4.8 18.66 1.7 23.53 18.23 13.62 34.21 22.75 51.08 20.53 10.36-1.36 19.49-17.76 25.96-32.64 4.43-10.25 4.43-21.31-2.06-24.9m-181.14-104.96c5.77-5.48-28.74-12.68-55.52 5.58-19.75 13.47-20.38 42.35-1.47 58.72 1.89 1.62 3.45 2.77 4.91 3.71 5.52-2.6 11.81-5.23 19.05-7.58 12.23-3.97 22.4-6.02 30.76-7.11 4-4.47 8.65-12.34 7.49-26.59-1.58-19.33-16.23-16.26-5.22-26.73" fill="#632ca6"/></svg>
|
After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
1
frontend/src/assets/icons/jira.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="2500" preserveAspectRatio="xMidYMid" width="2500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 -30.632388516510233 255.324 285.95638851651023"><linearGradient id="a"><stop offset=".18" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient><linearGradient id="b" x1="98.031%" x2="58.888%" xlink:href="#a" y1=".161%" y2="40.766%"/><linearGradient id="c" x1="100.665%" x2="55.402%" xlink:href="#a" y1=".455%" y2="44.727%"/><path d="M244.658 0H121.707a55.502 55.502 0 0 0 55.502 55.502h22.649V77.37c.02 30.625 24.841 55.447 55.466 55.467V10.666C255.324 4.777 250.55 0 244.658 0z" fill="#2684ff"/><path d="M183.822 61.262H60.872c.019 30.625 24.84 55.447 55.466 55.467h22.649v21.938c.039 30.625 24.877 55.43 55.502 55.43V71.93c0-5.891-4.776-10.667-10.667-10.667z" fill="url(#b)"/><path d="M122.951 122.489H0c0 30.653 24.85 55.502 55.502 55.502h22.72v21.867c.02 30.597 24.798 55.408 55.396 55.466V133.156c0-5.891-4.776-10.667-10.667-10.667z" fill="url(#c)"/></svg>
|
After Width: | Height: | Size: 1.0 KiB |
1
frontend/src/assets/icons/slack.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg enable-background="new 0 0 2447.6 2452.5" viewBox="0 0 2447.6 2452.5" xmlns="http://www.w3.org/2000/svg"><g clip-rule="evenodd" fill-rule="evenodd"><path d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z" fill="#36c5f0"/><path d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z" fill="#2eb67d"/><path d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z" fill="#ecb22e"/><path d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0" fill="#e01e5a"/></g></svg>
|
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 419 B After Width: | Height: | Size: 419 B |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
59
frontend/src/assets/icons/teams.svg
Normal file
@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
|
||||
<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
|
||||
<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
|
||||
<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
|
||||
<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
|
||||
<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
|
||||
<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
|
||||
<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
|
||||
]>
|
||||
<svg version="1.1" id="Livello_1" xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 2228.833 2073.333"
|
||||
enable-background="new 0 0 2228.833 2073.333" xml:space="preserve">
|
||||
<metadata>
|
||||
<sfw xmlns="&ns_sfw;">
|
||||
<slices></slices>
|
||||
<sliceSourceBounds bottomLeftOrigin="true" height="2073.333" width="2228.833" x="-1116.333" y="-1012.667">
|
||||
</sliceSourceBounds>
|
||||
</sfw>
|
||||
</metadata>
|
||||
<path fill="#5059C9" d="M1554.637,777.5h575.713c54.391,0,98.483,44.092,98.483,98.483c0,0,0,0,0,0v524.398
|
||||
c0,199.901-162.051,361.952-361.952,361.952h0h-1.711c-199.901,0.028-361.975-162-362.004-361.901c0-0.017,0-0.034,0-0.052V828.971
|
||||
C1503.167,800.544,1526.211,777.5,1554.637,777.5L1554.637,777.5z"/>
|
||||
<circle fill="#5059C9" cx="1943.75" cy="440.583" r="233.25"/>
|
||||
<circle fill="#7B83EB" cx="1218.083" cy="336.917" r="336.917"/>
|
||||
<path fill="#7B83EB" d="M1667.323,777.5H717.01c-53.743,1.33-96.257,45.931-95.01,99.676v598.105
|
||||
c-7.505,322.519,247.657,590.16,570.167,598.053c322.51-7.893,577.671-275.534,570.167-598.053V877.176
|
||||
C1763.579,823.431,1721.066,778.83,1667.323,777.5z"/>
|
||||
<path opacity="0.1" enable-background="new " d="M1244,777.5v838.145c-0.258,38.435-23.549,72.964-59.09,87.598
|
||||
c-11.316,4.787-23.478,7.254-35.765,7.257H667.613c-6.738-17.105-12.958-34.21-18.142-51.833
|
||||
c-18.144-59.477-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1244z"/>
|
||||
<path opacity="0.2" enable-background="new " d="M1192.167,777.5v889.978c-0.002,12.287-2.47,24.449-7.257,35.765
|
||||
c-14.634,35.541-49.163,58.833-87.598,59.09H691.975c-8.812-17.105-17.105-34.21-24.362-51.833
|
||||
c-7.257-17.623-12.958-34.21-18.142-51.833c-18.144-59.476-27.402-121.307-27.472-183.49V877.02
|
||||
c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"/>
|
||||
<path opacity="0.2" enable-background="new " d="M1192.167,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855h-447.84
|
||||
c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"/>
|
||||
<path opacity="0.2" enable-background="new " d="M1140.333,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855H649.472
|
||||
c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1140.333z"/>
|
||||
<path opacity="0.1" enable-background="new " d="M1244,509.522v163.275c-8.812,0.518-17.105,1.037-25.917,1.037
|
||||
c-8.812,0-17.105-0.518-25.917-1.037c-17.496-1.161-34.848-3.937-51.833-8.293c-104.963-24.857-191.679-98.469-233.25-198.003
|
||||
c-7.153-16.715-12.706-34.071-16.587-51.833h258.648C1201.449,414.866,1243.801,457.217,1244,509.522z"/>
|
||||
<path opacity="0.2" enable-background="new " d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293
|
||||
c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"/>
|
||||
<path opacity="0.2" enable-background="new " d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293
|
||||
c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"/>
|
||||
<path opacity="0.2" enable-background="new " d="M1140.333,561.355v103.148c-104.963-24.857-191.679-98.469-233.25-198.003
|
||||
h138.395C1097.783,466.699,1140.134,509.051,1140.333,561.355z"/>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="198.099" y1="1683.0726" x2="942.2344" y2="394.2607" gradientTransform="matrix(1 0 0 -1 0 2075.3333)">
|
||||
<stop offset="0" style="stop-color:#5A62C3"/>
|
||||
<stop offset="0.5" style="stop-color:#4D55BD"/>
|
||||
<stop offset="1" style="stop-color:#3940AB"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_1_)" d="M95.01,466.5h950.312c52.473,0,95.01,42.538,95.01,95.01v950.312c0,52.473-42.538,95.01-95.01,95.01
|
||||
H95.01c-52.473,0-95.01-42.538-95.01-95.01V561.51C0,509.038,42.538,466.5,95.01,466.5z"/>
|
||||
<path fill="#FFFFFF" d="M820.211,828.193H630.241v517.297H509.211V828.193H320.123V727.844h500.088V828.193z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 229 B After Width: | Height: | Size: 229 B |
1
frontend/src/assets/icons/webhooks.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="2500" height="2334" viewBox="0 0 256 239" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M119.54 100.503c-10.61 17.836-20.775 35.108-31.152 52.25-2.665 4.401-3.984 7.986-1.855 13.58 5.878 15.454-2.414 30.493-17.998 34.575-14.697 3.851-29.016-5.808-31.932-21.543-2.584-13.927 8.224-27.58 23.58-29.757 1.286-.184 2.6-.205 4.762-.367l23.358-39.168C73.612 95.465 64.868 78.39 66.803 57.23c1.368-14.957 7.25-27.883 18-38.477 20.59-20.288 52.002-23.573 76.246-8.001 23.284 14.958 33.948 44.094 24.858 69.031-6.854-1.858-13.756-3.732-21.343-5.79 2.854-13.865.743-26.315-8.608-36.981-6.178-7.042-14.106-10.733-23.12-12.093-18.072-2.73-35.815 8.88-41.08 26.618-5.976 20.13 3.069 36.575 27.784 48.967z" fill="#C73A63"/><path d="M149.841 79.41c7.475 13.187 15.065 26.573 22.587 39.836 38.02-11.763 66.686 9.284 76.97 31.817 12.422 27.219 3.93 59.457-20.465 76.25-25.04 17.238-56.707 14.293-78.892-7.851 5.654-4.733 11.336-9.487 17.407-14.566 21.912 14.192 41.077 13.524 55.305-3.282 12.133-14.337 11.87-35.714-.615-49.75-14.408-16.197-33.707-16.691-57.035-1.143-9.677-17.168-19.522-34.199-28.893-51.491-3.16-5.828-6.648-9.21-13.77-10.443-11.893-2.062-19.571-12.275-20.032-23.717-.453-11.316 6.214-21.545 16.634-25.53 10.322-3.949 22.435-.762 29.378 8.014 5.674 7.17 7.477 15.24 4.491 24.083-.83 2.466-1.905 4.852-3.07 7.774z" fill="#4B4B4B"/><path d="M167.707 187.21h-45.77c-4.387 18.044-13.863 32.612-30.19 41.876-12.693 7.2-26.373 9.641-40.933 7.29-26.808-4.323-48.728-28.456-50.658-55.63-2.184-30.784 18.975-58.147 47.178-64.293 1.947 7.071 3.915 14.21 5.862 21.264-25.876 13.202-34.832 29.836-27.59 50.636 6.375 18.304 24.484 28.337 44.147 24.457 20.08-3.962 30.204-20.65 28.968-47.432 19.036 0 38.088-.197 57.126.097 7.434.117 13.173-.654 18.773-7.208 9.22-10.784 26.191-9.811 36.121.374 10.148 10.409 9.662 27.157-1.077 37.127-10.361 9.62-26.73 9.106-36.424-1.26-1.992-2.136-3.562-4.673-5.533-7.298z" fill="#4A4A4A"/></svg>
|
After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/src/assets/img/logo.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
@ -1,7 +1,7 @@
|
||||
import { FC } from "react";
|
||||
import { FC } from 'react';
|
||||
|
||||
import AccessContext from '../../contexts/AccessContext'
|
||||
import { ADMIN } from "./permissions";
|
||||
import AccessContext from '../../contexts/AccessContext';
|
||||
import { ADMIN } from './permissions';
|
||||
|
||||
// TODO: Type up redux store
|
||||
interface IAccessProvider {
|
||||
@ -9,30 +9,34 @@ interface IAccessProvider {
|
||||
}
|
||||
|
||||
interface IPermission {
|
||||
permission: string;
|
||||
project: string | null;
|
||||
permission: string;
|
||||
project: string | null;
|
||||
}
|
||||
|
||||
const AccessProvider: FC<IAccessProvider> = ({store, children}) => {
|
||||
const hasAccess = (permission: string, project: string) => {
|
||||
const permissions = store.getState().user.get('permissions') || [];
|
||||
const AccessProvider: FC<IAccessProvider> = ({ store, children }) => {
|
||||
const hasAccess = (permission: string, project: string) => {
|
||||
const permissions = store.getState().user.get('permissions') || [];
|
||||
|
||||
const result = permissions.some((p: IPermission) => {
|
||||
if(p.permission === ADMIN) {
|
||||
return true
|
||||
}
|
||||
if(p.permission === permission && p.project === project) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const result = permissions.some((p: IPermission) => {
|
||||
if (p.permission === ADMIN) {
|
||||
return true;
|
||||
}
|
||||
if (p.permission === permission && p.project === project) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
return result;
|
||||
};
|
||||
|
||||
const context = { hasAccess };
|
||||
const context = { hasAccess };
|
||||
|
||||
return <AccessContext.Provider value={context}>{children}</AccessContext.Provider>
|
||||
}
|
||||
return (
|
||||
<AccessContext.Provider value={context}>
|
||||
{children}
|
||||
</AccessContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessProvider;
|
||||
export default AccessProvider;
|
||||
|
@ -10,11 +10,18 @@ import { routes } from './menu/routes';
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
import IAuthStatus from '../interfaces/user';
|
||||
import { useEffect } from 'react';
|
||||
interface IAppProps extends RouteComponentProps {
|
||||
user: IAuthStatus;
|
||||
fetchUiBootstrap: any;
|
||||
}
|
||||
|
||||
const App = ({ location, user }: IAppProps) => {
|
||||
const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
||||
useEffect(() => {
|
||||
fetchUiBootstrap();
|
||||
/* eslint-disable-next-line */
|
||||
}, []);
|
||||
|
||||
const renderMainLayoutRoutes = () => {
|
||||
return routes.filter(route => route.layout === 'main').map(renderRoute);
|
||||
};
|
||||
|
14
frontend/src/component/AppContainer.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { connect } from 'react-redux';
|
||||
import App from './App';
|
||||
|
||||
import { fetchUiBootstrap } from '../store/ui-bootstrap/actions';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchUiBootstrap,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: any) => ({
|
||||
user: state.user.toJS(),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
@ -6,6 +6,13 @@ import { Avatar, Icon } from '@material-ui/core';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||
import AccessContext from '../../../contexts/AccessContext';
|
||||
|
||||
import slackIcon from '../../../assets/icons/slack.svg';
|
||||
import jiraIcon from '../../../assets/icons/jira.svg';
|
||||
import webhooksIcon from '../../../assets/icons/webhooks.svg';
|
||||
import teamsIcon from '../../../assets/icons/teams.svg';
|
||||
import dataDogIcon from '../../../assets/icons/datadog.svg';
|
||||
import { formatAssetPath } from '../../../utils/format-path';
|
||||
|
||||
const style = {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
@ -16,15 +23,45 @@ const style = {
|
||||
const getIcon = name => {
|
||||
switch (name) {
|
||||
case 'slack':
|
||||
return <img style={style} alt="Slack Logo" src="slack.svg" />;
|
||||
return (
|
||||
<img
|
||||
style={style}
|
||||
alt="Slack Logo"
|
||||
src={formatAssetPath(slackIcon)}
|
||||
/>
|
||||
);
|
||||
case 'jira-comment':
|
||||
return <img style={style} alt="JIRA Logo" src="jira.svg" />;
|
||||
return (
|
||||
<img
|
||||
style={style}
|
||||
alt="JIRA Logo"
|
||||
src={formatAssetPath(jiraIcon)}
|
||||
/>
|
||||
);
|
||||
case 'webhook':
|
||||
return <img style={style} alt="Generic Webhook logo" src="webhooks.svg" />;
|
||||
return (
|
||||
<img
|
||||
style={style}
|
||||
alt="Generic Webhook logo"
|
||||
src={formatAssetPath(webhooksIcon)}
|
||||
/>
|
||||
);
|
||||
case 'teams':
|
||||
return <img style={style} alt="Microsoft Teams Logo" src="teams.svg" />;
|
||||
return (
|
||||
<img
|
||||
style={style}
|
||||
alt="Microsoft Teams Logo"
|
||||
src={formatAssetPath(teamsIcon)}
|
||||
/>
|
||||
);
|
||||
case 'datadog':
|
||||
return <img style={style} alt="Datadog" src="datadog.svg" />;
|
||||
return (
|
||||
<img
|
||||
style={style}
|
||||
alt="Datadog"
|
||||
src={formatAssetPath(dataDogIcon)}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Avatar>
|
||||
@ -34,7 +71,14 @@ const getIcon = name => {
|
||||
}
|
||||
};
|
||||
|
||||
const AddonList = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, history }) => {
|
||||
const AddonList = ({
|
||||
addons,
|
||||
providers,
|
||||
fetchAddons,
|
||||
removeAddon,
|
||||
toggleAddon,
|
||||
history,
|
||||
}) => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
useEffect(() => {
|
||||
if (addons.length === 0) {
|
||||
@ -59,7 +103,12 @@ const AddonList = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, h
|
||||
/>
|
||||
|
||||
<br />
|
||||
<AvailableAddons providers={providers} hasAccess={hasAccess} history={history} getIcon={getIcon} />
|
||||
<AvailableAddons
|
||||
providers={providers}
|
||||
hasAccess={hasAccess}
|
||||
history={history}
|
||||
getIcon={getIcon}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,13 @@
|
||||
import React from 'react';
|
||||
import PageContent from '../../../common/PageContent/PageContent';
|
||||
import { Button, List, ListItem, ListItemAvatar, ListItemSecondaryAction, ListItemText } from '@material-ui/core';
|
||||
import {
|
||||
Button,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
} from '@material-ui/core';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||
import { CREATE_ADDON } from '../../../AccessProvider/permissions';
|
||||
import PropTypes from 'prop-types';
|
||||
@ -9,7 +16,10 @@ const AvailableAddons = ({ providers, getIcon, hasAccess, history }) => {
|
||||
const renderProvider = provider => (
|
||||
<ListItem key={provider.name}>
|
||||
<ListItemAvatar>{getIcon(provider.name)}</ListItemAvatar>
|
||||
<ListItemText primary={provider.displayName} secondary={provider.description} />
|
||||
<ListItemText
|
||||
primary={provider.displayName}
|
||||
secondary={provider.description}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(CREATE_ADDON)}
|
||||
@ -17,7 +27,9 @@ const AvailableAddons = ({ providers, getIcon, hasAccess, history }) => {
|
||||
<Button
|
||||
variant="contained"
|
||||
name="device_hub"
|
||||
onClick={() => history.push(`/addons/create/${provider.name}`)}
|
||||
onClick={() =>
|
||||
history.push(`/addons/create/${provider.name}`)
|
||||
}
|
||||
title="Configure"
|
||||
>
|
||||
Configure
|
||||
|
@ -494,7 +494,8 @@ exports[`renders correctly with permissions 1`] = `
|
||||
>
|
||||
<span>
|
||||
123.123.123.123
|
||||
last seen at
|
||||
last seen at
|
||||
|
||||
<small>
|
||||
02/23/2017, 03:56:49 PM
|
||||
</small>
|
||||
|
@ -4,7 +4,12 @@ import { ThemeProvider } from '@material-ui/core';
|
||||
import ClientApplications from '../application-edit-component';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ADMIN, CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../AccessProvider/permissions';
|
||||
import {
|
||||
ADMIN,
|
||||
CREATE_FEATURE,
|
||||
CREATE_STRATEGY,
|
||||
UPDATE_APPLICATION,
|
||||
} from '../../AccessProvider/permissions';
|
||||
import theme from '../../../themes/main-theme';
|
||||
|
||||
import { createFakeStore } from '../../../accessStoreFake';
|
||||
@ -13,7 +18,7 @@ import AccessProvider from '../../AccessProvider/AccessProvider';
|
||||
test('renders correctly if no application', () => {
|
||||
const tree = renderer
|
||||
.create(
|
||||
<AccessProvider store={createFakeStore([{permission: ADMIN}])}>
|
||||
<AccessProvider store={createFakeStore([{ permission: ADMIN }])}>
|
||||
<ClientApplications
|
||||
fetchApplication={() => Promise.resolve({})}
|
||||
storeApplicationMetaData={jest.fn()}
|
||||
@ -32,51 +37,51 @@ test('renders correctly without permission', () => {
|
||||
.create(
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AccessProvider store={createFakeStore([])}>
|
||||
<ClientApplications
|
||||
fetchApplication={() => Promise.resolve({})}
|
||||
storeApplicationMetaData={jest.fn()}
|
||||
deleteApplication={jest.fn()}
|
||||
history={{}}
|
||||
application={{
|
||||
appName: 'test-app',
|
||||
instances: [
|
||||
{
|
||||
instanceId: 'instance-1',
|
||||
clientIp: '123.123.123.123',
|
||||
lastSeen: '2017-02-23T15:56:49',
|
||||
sdkVersion: '4.0',
|
||||
},
|
||||
],
|
||||
strategies: [
|
||||
{
|
||||
name: 'StrategyA',
|
||||
description: 'A description',
|
||||
},
|
||||
{
|
||||
name: 'StrategyB',
|
||||
description: 'B description',
|
||||
notFound: true,
|
||||
},
|
||||
],
|
||||
seenToggles: [
|
||||
{
|
||||
name: 'ToggleA',
|
||||
description: 'this is A toggle',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'ToggleB',
|
||||
description: 'this is B toggle',
|
||||
enabled: false,
|
||||
notFound: true,
|
||||
},
|
||||
],
|
||||
url: 'http://example.org',
|
||||
description: 'app description',
|
||||
}}
|
||||
location={{ locale: 'en-GB' }}
|
||||
/>
|
||||
<AccessProvider store={createFakeStore([])}>
|
||||
<ClientApplications
|
||||
fetchApplication={() => Promise.resolve({})}
|
||||
storeApplicationMetaData={jest.fn()}
|
||||
deleteApplication={jest.fn()}
|
||||
history={{}}
|
||||
application={{
|
||||
appName: 'test-app',
|
||||
instances: [
|
||||
{
|
||||
instanceId: 'instance-1',
|
||||
clientIp: '123.123.123.123',
|
||||
lastSeen: '2017-02-23T15:56:49',
|
||||
sdkVersion: '4.0',
|
||||
},
|
||||
],
|
||||
strategies: [
|
||||
{
|
||||
name: 'StrategyA',
|
||||
description: 'A description',
|
||||
},
|
||||
{
|
||||
name: 'StrategyB',
|
||||
description: 'B description',
|
||||
notFound: true,
|
||||
},
|
||||
],
|
||||
seenToggles: [
|
||||
{
|
||||
name: 'ToggleA',
|
||||
description: 'this is A toggle',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'ToggleB',
|
||||
description: 'this is B toggle',
|
||||
enabled: false,
|
||||
notFound: true,
|
||||
},
|
||||
],
|
||||
url: 'http://example.org',
|
||||
description: 'app description',
|
||||
}}
|
||||
location={{ locale: 'en-GB' }}
|
||||
/>
|
||||
</AccessProvider>
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
@ -91,51 +96,53 @@ test('renders correctly with permissions', () => {
|
||||
.create(
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AccessProvider store={createFakeStore([{permission: ADMIN}])}>
|
||||
<ClientApplications
|
||||
fetchApplication={() => Promise.resolve({})}
|
||||
storeApplicationMetaData={jest.fn()}
|
||||
history={{}}
|
||||
deleteApplication={jest.fn()}
|
||||
application={{
|
||||
appName: 'test-app',
|
||||
instances: [
|
||||
{
|
||||
instanceId: 'instance-1',
|
||||
clientIp: '123.123.123.123',
|
||||
lastSeen: '2017-02-23T15:56:49',
|
||||
sdkVersion: '4.0',
|
||||
},
|
||||
],
|
||||
strategies: [
|
||||
{
|
||||
name: 'StrategyA',
|
||||
description: 'A description',
|
||||
},
|
||||
{
|
||||
name: 'StrategyB',
|
||||
description: 'B description',
|
||||
notFound: true,
|
||||
},
|
||||
],
|
||||
seenToggles: [
|
||||
{
|
||||
name: 'ToggleA',
|
||||
description: 'this is A toggle',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'ToggleB',
|
||||
description: 'this is B toggle',
|
||||
enabled: false,
|
||||
notFound: true,
|
||||
},
|
||||
],
|
||||
url: 'http://example.org',
|
||||
description: 'app description',
|
||||
}}
|
||||
location={{ locale: 'en-GB' }}
|
||||
/>
|
||||
<AccessProvider
|
||||
store={createFakeStore([{ permission: ADMIN }])}
|
||||
>
|
||||
<ClientApplications
|
||||
fetchApplication={() => Promise.resolve({})}
|
||||
storeApplicationMetaData={jest.fn()}
|
||||
history={{}}
|
||||
deleteApplication={jest.fn()}
|
||||
application={{
|
||||
appName: 'test-app',
|
||||
instances: [
|
||||
{
|
||||
instanceId: 'instance-1',
|
||||
clientIp: '123.123.123.123',
|
||||
lastSeen: '2017-02-23T15:56:49',
|
||||
sdkVersion: '4.0',
|
||||
},
|
||||
],
|
||||
strategies: [
|
||||
{
|
||||
name: 'StrategyA',
|
||||
description: 'A description',
|
||||
},
|
||||
{
|
||||
name: 'StrategyB',
|
||||
description: 'B description',
|
||||
notFound: true,
|
||||
},
|
||||
],
|
||||
seenToggles: [
|
||||
{
|
||||
name: 'ToggleA',
|
||||
description: 'this is A toggle',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'ToggleB',
|
||||
description: 'this is B toggle',
|
||||
enabled: false,
|
||||
notFound: true,
|
||||
},
|
||||
],
|
||||
url: 'http://example.org',
|
||||
description: 'app description',
|
||||
}}
|
||||
location={{ locale: 'en-GB' }}
|
||||
/>
|
||||
</AccessProvider>
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
|
@ -2,9 +2,20 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Avatar, Link, Icon, IconButton, Button, LinearProgress, Typography } from '@material-ui/core';
|
||||
import {
|
||||
Avatar,
|
||||
Link,
|
||||
Icon,
|
||||
IconButton,
|
||||
Button,
|
||||
LinearProgress,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||
import { formatFullDateTimeWithLocale, formatDateWithLocale } from '../common/util';
|
||||
import {
|
||||
formatFullDateTimeWithLocale,
|
||||
formatDateWithLocale,
|
||||
} from '../common/util';
|
||||
import { UPDATE_APPLICATION } from '../AccessProvider/permissions';
|
||||
import ApplicationView from './application-view';
|
||||
import ApplicationUpdate from './application-update';
|
||||
@ -37,9 +48,12 @@ class ClientApplications extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchApplication(this.props.appName).finally(() => this.setState({ loading: false }));
|
||||
this.props
|
||||
.fetchApplication(this.props.appName)
|
||||
.finally(() => this.setState({ loading: false }));
|
||||
}
|
||||
formatFullDateTime = v => formatFullDateTimeWithLocale(v, this.props.location.locale);
|
||||
formatFullDateTime = v =>
|
||||
formatFullDateTimeWithLocale(v, this.props.location.locale);
|
||||
formatDate = v => formatDateWithLocale(v, this.props.location.locale);
|
||||
|
||||
deleteApplication = async evt => {
|
||||
@ -64,7 +78,16 @@ class ClientApplications extends PureComponent {
|
||||
}
|
||||
const { hasAccess } = this.context;
|
||||
const { application, storeApplicationMetaData } = this.props;
|
||||
const { appName, instances, strategies, seenToggles, url, description, icon = 'apps', createdAt } = application;
|
||||
const {
|
||||
appName,
|
||||
instances,
|
||||
strategies,
|
||||
seenToggles,
|
||||
url,
|
||||
description,
|
||||
icon = 'apps',
|
||||
createdAt,
|
||||
} = application;
|
||||
|
||||
const toggleModal = () => {
|
||||
this.setState(prev => ({ ...prev, prompt: !prev.prompt }));
|
||||
@ -95,7 +118,10 @@ class ClientApplications extends PureComponent {
|
||||
{
|
||||
label: 'Edit application',
|
||||
component: (
|
||||
<ApplicationUpdate application={application} storeApplicationMetaData={storeApplicationMetaData} />
|
||||
<ApplicationUpdate
|
||||
application={application}
|
||||
storeApplicationMetaData={storeApplicationMetaData}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
@ -131,7 +157,11 @@ class ClientApplications extends PureComponent {
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(UPDATE_APPLICATION)}
|
||||
show={
|
||||
<Button color="secondary" title="Delete application" onClick={toggleModal}>
|
||||
<Button
|
||||
color="secondary"
|
||||
title="Delete application"
|
||||
onClick={toggleModal}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
}
|
||||
|
@ -1,12 +1,27 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Grid, List, ListItem, ListItemText, ListItemAvatar, Switch, Icon, Typography } from '@material-ui/core';
|
||||
import {
|
||||
Grid,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemAvatar,
|
||||
Switch,
|
||||
Icon,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { shorten } from '../common';
|
||||
import { CREATE_FEATURE, CREATE_STRATEGY } from '../AccessProvider/permissions';
|
||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
function ApplicationView({ seenToggles, hasAccess, strategies, instances, formatFullDateTime }) {
|
||||
function ApplicationView({
|
||||
seenToggles,
|
||||
hasAccess,
|
||||
strategies,
|
||||
instances,
|
||||
formatFullDateTime,
|
||||
}) {
|
||||
const notFoundListItem = ({ createUrl, name, permission }) => (
|
||||
<ConditionallyRender
|
||||
key={`not_found_conditional_${name}`}
|
||||
@ -17,7 +32,9 @@ function ApplicationView({ seenToggles, hasAccess, strategies, instances, format
|
||||
<Icon>report</Icon>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={<Link to={`${createUrl}?name=${name}`}>{name}</Link>}
|
||||
primary={
|
||||
<Link to={`${createUrl}?name=${name}`}>{name}</Link>
|
||||
}
|
||||
secondary={'Missing, want to create?'}
|
||||
/>
|
||||
</ListItem>
|
||||
@ -27,14 +44,24 @@ function ApplicationView({ seenToggles, hasAccess, strategies, instances, format
|
||||
<ListItemAvatar>
|
||||
<Icon>report</Icon>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={name} secondary={`Could not find feature toggle with name ${name}`} />
|
||||
<ListItemText
|
||||
primary={name}
|
||||
secondary={`Could not find feature toggle with name ${name}`}
|
||||
/>
|
||||
</ListItem>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const foundListItem = ({ viewUrl, name, showSwitch, enabled, description, i }) => (
|
||||
const foundListItem = ({
|
||||
viewUrl,
|
||||
name,
|
||||
showSwitch,
|
||||
enabled,
|
||||
description,
|
||||
i,
|
||||
}) => (
|
||||
<ListItem key={`found_${name}-${i}`}>
|
||||
<ListItemAvatar>
|
||||
<ConditionallyRender
|
||||
@ -45,7 +72,9 @@ function ApplicationView({ seenToggles, hasAccess, strategies, instances, format
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={<Link to={`${viewUrl}/${name}`}>{shorten(name, 50)}</Link>}
|
||||
primary={
|
||||
<Link to={`${viewUrl}/${name}`}>{shorten(name, 50)}</Link>
|
||||
}
|
||||
secondary={shorten(description, 60)}
|
||||
/>
|
||||
</ListItem>
|
||||
@ -58,26 +87,28 @@ function ApplicationView({ seenToggles, hasAccess, strategies, instances, format
|
||||
</Typography>
|
||||
<hr />
|
||||
<List>
|
||||
{seenToggles.map(({ name, description, enabled, notFound }, i) => (
|
||||
<ConditionallyRender
|
||||
key={`toggle_conditional_${name}`}
|
||||
condition={notFound}
|
||||
show={notFoundListItem({
|
||||
createUrl: '/features/create',
|
||||
name,
|
||||
permission: CREATE_FEATURE,
|
||||
i,
|
||||
})}
|
||||
elseShow={foundListItem({
|
||||
viewUrl: '/features/strategies',
|
||||
name,
|
||||
showSwitch: true,
|
||||
enabled,
|
||||
description,
|
||||
i,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
{seenToggles.map(
|
||||
({ name, description, enabled, notFound }, i) => (
|
||||
<ConditionallyRender
|
||||
key={`toggle_conditional_${name}`}
|
||||
condition={notFound}
|
||||
show={notFoundListItem({
|
||||
createUrl: '/features/create',
|
||||
name,
|
||||
permission: CREATE_FEATURE,
|
||||
i,
|
||||
})}
|
||||
elseShow={foundListItem({
|
||||
viewUrl: '/features/strategies',
|
||||
name,
|
||||
showSwitch: true,
|
||||
enabled,
|
||||
description,
|
||||
i,
|
||||
})}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</List>
|
||||
</Grid>
|
||||
<Grid item xl={6} md={6} xs={12}>
|
||||
@ -114,28 +145,33 @@ function ApplicationView({ seenToggles, hasAccess, strategies, instances, format
|
||||
</Typography>
|
||||
<hr />
|
||||
<List>
|
||||
{instances.map(({ instanceId, clientIp, lastSeen, sdkVersion }) => (
|
||||
<ListItem key={`${instanceId}`}>
|
||||
<ListItemAvatar>
|
||||
<Icon>timeline</Icon>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<ConditionallyRender
|
||||
key={`${instanceId}_conditional`}
|
||||
condition={sdkVersion}
|
||||
show={`${instanceId} (${sdkVersion})`}
|
||||
elseShow={instanceId}
|
||||
/>
|
||||
}
|
||||
secondary={
|
||||
<span>
|
||||
{clientIp} last seen at <small>{formatFullDateTime(lastSeen)}</small>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
{instances.map(
|
||||
({ instanceId, clientIp, lastSeen, sdkVersion }) => (
|
||||
<ListItem key={`${instanceId}`}>
|
||||
<ListItemAvatar>
|
||||
<Icon>timeline</Icon>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<ConditionallyRender
|
||||
key={`${instanceId}_conditional`}
|
||||
condition={sdkVersion}
|
||||
show={`${instanceId} (${sdkVersion})`}
|
||||
elseShow={instanceId}
|
||||
/>
|
||||
}
|
||||
secondary={
|
||||
<span>
|
||||
{clientIp} last seen at{' '}
|
||||
<small>
|
||||
{formatFullDateTime(lastSeen)}
|
||||
</small>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
)
|
||||
)}
|
||||
</List>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
@ -0,0 +1,15 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
proclamation: {
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
content: {
|
||||
maxWidth: '800px',
|
||||
},
|
||||
link: {
|
||||
display: 'block',
|
||||
marginTop: '0.5rem',
|
||||
width: '100px',
|
||||
},
|
||||
});
|
68
frontend/src/component/common/Proclamation/Proclamation.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import { useState } from 'react';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import ConditionallyRender from '../ConditionallyRender';
|
||||
import { Typography } from '@material-ui/core';
|
||||
import { useStyles } from './Proclamation.styles';
|
||||
|
||||
interface IProclamationProps {
|
||||
toast?: IToast;
|
||||
}
|
||||
|
||||
interface IToast {
|
||||
message: string;
|
||||
id: string;
|
||||
severity: 'success' | 'info' | 'warning' | 'error';
|
||||
link: string;
|
||||
}
|
||||
|
||||
const renderProclamation = (id: string) => {
|
||||
if (!id) return false;
|
||||
if (localStorage) {
|
||||
const value = localStorage.getItem(id);
|
||||
if (value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const Proclamation = ({ toast }: IProclamationProps) => {
|
||||
const [show, setShow] = useState(renderProclamation(toast?.id || ''));
|
||||
const styles = useStyles();
|
||||
|
||||
const onClose = () => {
|
||||
if (localStorage) {
|
||||
localStorage.setItem(toast?.id || '', 'seen');
|
||||
}
|
||||
setShow(false);
|
||||
};
|
||||
|
||||
if (!toast) return null;
|
||||
|
||||
return (
|
||||
<ConditionallyRender
|
||||
condition={show}
|
||||
show={
|
||||
<Alert
|
||||
className={styles.proclamation}
|
||||
severity={toast.severity}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Typography className={styles.content} variant="body2">
|
||||
{toast.message}
|
||||
</Typography>
|
||||
<a
|
||||
href={toast.link}
|
||||
className={styles.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View more
|
||||
</a>
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Proclamation;
|
@ -11,7 +11,7 @@ const ProtectedRoute = ({
|
||||
{...rest}
|
||||
render={props => {
|
||||
if (unauthorized) {
|
||||
return <Redirect to="/login" />;
|
||||
return <Redirect to={'/login'} />;
|
||||
} else {
|
||||
return <Component {...props} {...renderProps} />;
|
||||
}
|
||||
|
@ -2,9 +2,20 @@ import PropTypes from 'prop-types';
|
||||
import PageContent from '../../common/PageContent/PageContent';
|
||||
import HeaderTitle from '../../common/HeaderTitle';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||
import { CREATE_CONTEXT_FIELD, DELETE_CONTEXT_FIELD } from '../../AccessProvider/permissions';
|
||||
import { Icon, IconButton, List, ListItem, ListItemIcon, ListItemText, Tooltip } from '@material-ui/core';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import {
|
||||
CREATE_CONTEXT_FIELD,
|
||||
DELETE_CONTEXT_FIELD,
|
||||
} from '../../AccessProvider/permissions';
|
||||
import {
|
||||
Icon,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
} from '@material-ui/core';
|
||||
import { useContext, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useStyles } from './styles';
|
||||
import ConfirmDialogue from '../../common/Dialogue';
|
||||
@ -61,7 +72,14 @@ const ContextList = ({ removeContextField, history, contextFields }) => {
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<PageContent headerContent={<HeaderTitle actions={headerButton()} title={'Context fields'} />}>
|
||||
<PageContent
|
||||
headerContent={
|
||||
<HeaderTitle
|
||||
actions={headerButton()}
|
||||
title={'Context fields'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<List>
|
||||
<ConditionallyRender
|
||||
condition={contextFields.length > 0}
|
||||
|
@ -282,7 +282,6 @@ class AddContextComponent extends Component {
|
||||
Read more
|
||||
</a>
|
||||
</p>
|
||||
{console.log(contextField.stickiness)}
|
||||
<Switch
|
||||
label="Allow stickiness"
|
||||
checked={contextField.stickiness}
|
||||
|
@ -27,7 +27,15 @@ const FeatureToggleListItem = ({
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
|
||||
const { name, description, enabled, type, stale, createdAt, project } = feature;
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
enabled,
|
||||
type,
|
||||
stale,
|
||||
createdAt,
|
||||
project,
|
||||
} = feature;
|
||||
const { showLastHour = false } = settings;
|
||||
const isStale = showLastHour
|
||||
? metricsLastHour.isFallback
|
||||
|
@ -9,8 +9,6 @@ import { createFakeStore } from '../../../../accessStoreFake';
|
||||
import { ADMIN, CREATE_FEATURE } from '../../../AccessProvider/permissions';
|
||||
import AccessProvider from '../../../AccessProvider/AccessProvider';
|
||||
|
||||
|
||||
|
||||
jest.mock('../FeatureToggleListItem', () => ({
|
||||
__esModule: true,
|
||||
default: 'ListItem',
|
||||
@ -29,17 +27,19 @@ test('renders correctly with one feature', () => {
|
||||
const tree = renderer.create(
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AccessProvider store={createFakeStore([{permission: CREATE_FEATURE}])}>
|
||||
<FeatureToggleList
|
||||
updateSetting={jest.fn()}
|
||||
settings={settings}
|
||||
history={{}}
|
||||
featureMetrics={featureMetrics}
|
||||
features={features}
|
||||
toggleFeature={jest.fn()}
|
||||
fetcher={jest.fn()}
|
||||
currentProjectId='default'
|
||||
/>
|
||||
<AccessProvider
|
||||
store={createFakeStore([{ permission: CREATE_FEATURE }])}
|
||||
>
|
||||
<FeatureToggleList
|
||||
updateSetting={jest.fn()}
|
||||
settings={settings}
|
||||
history={{}}
|
||||
featureMetrics={featureMetrics}
|
||||
features={features}
|
||||
toggleFeature={jest.fn()}
|
||||
fetcher={jest.fn()}
|
||||
currentProjectId="default"
|
||||
/>
|
||||
</AccessProvider>
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
@ -59,17 +59,19 @@ test('renders correctly with one feature without permissions', () => {
|
||||
const tree = renderer.create(
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AccessProvider store={createFakeStore([{permission: CREATE_FEATURE}])}>
|
||||
<FeatureToggleList
|
||||
updateSetting={jest.fn()}
|
||||
settings={settings}
|
||||
history={{}}
|
||||
featureMetrics={featureMetrics}
|
||||
features={features}
|
||||
toggleFeature={jest.fn()}
|
||||
fetcher={jest.fn()}
|
||||
currentProjectId='default'
|
||||
/>
|
||||
<AccessProvider
|
||||
store={createFakeStore([{ permission: CREATE_FEATURE }])}
|
||||
>
|
||||
<FeatureToggleList
|
||||
updateSetting={jest.fn()}
|
||||
settings={settings}
|
||||
history={{}}
|
||||
featureMetrics={featureMetrics}
|
||||
features={features}
|
||||
toggleFeature={jest.fn()}
|
||||
fetcher={jest.fn()}
|
||||
currentProjectId="default"
|
||||
/>
|
||||
</AccessProvider>
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
|
@ -58,7 +58,7 @@ const FeatureView = ({
|
||||
const [delDialog, setDelDialog] = useState(false);
|
||||
const commonStyles = useCommonStyles();
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const { project } = featureToggle || { };
|
||||
const { project } = featureToggle || {};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToTop();
|
||||
@ -82,12 +82,14 @@ const FeatureView = ({
|
||||
const getTabComponent = key => {
|
||||
switch (key) {
|
||||
case 'activation':
|
||||
return <UpdateStrategies
|
||||
return (
|
||||
<UpdateStrategies
|
||||
featureToggle={featureToggle}
|
||||
features={features}
|
||||
history={history}
|
||||
editable={editable}
|
||||
/>
|
||||
);
|
||||
case 'metrics':
|
||||
return <MetricComponent featureToggle={featureToggle} />;
|
||||
case 'variants':
|
||||
|
@ -3,7 +3,14 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Button, Icon, TextField, Switch, Paper, FormControlLabel } from '@material-ui/core';
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
TextField,
|
||||
Switch,
|
||||
Paper,
|
||||
FormControlLabel,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { styles as commonStyles } from '../../common';
|
||||
import styles from './copy-feature-component.module.scss';
|
||||
@ -75,7 +82,11 @@ class CopyFeatureComponent extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
this.props.createFeatureToggle(copyToggle).then(() => history.push(`/features/strategies/${copyToggle.name}`));
|
||||
this.props
|
||||
.createFeatureToggle(copyToggle)
|
||||
.then(() =>
|
||||
history.push(`/features/strategies/${copyToggle.name}`)
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -86,17 +97,23 @@ class CopyFeatureComponent extends Component {
|
||||
const { newToggleName, nameError, replaceGroupId } = this.state;
|
||||
|
||||
return (
|
||||
<Paper className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||
<Paper
|
||||
className={commonStyles.fullwidth}
|
||||
style={{ overflow: 'visible' }}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<h1>Copy {copyToggle.name}</h1>
|
||||
</div>
|
||||
|
||||
<section className={styles.content}>
|
||||
<p className={styles.text}>
|
||||
You are about to create a new feature toggle by cloning the configuration of feature
|
||||
toggle
|
||||
<Link to={`/features/strategies/${copyToggle.name}`}>{copyToggle.name}</Link>. You must give the
|
||||
new feature toggle a unique name before you can proceed.
|
||||
You are about to create a new feature toggle by cloning
|
||||
the configuration of feature toggle
|
||||
<Link to={`/features/strategies/${copyToggle.name}`}>
|
||||
{copyToggle.name}
|
||||
</Link>
|
||||
. You must give the new feature toggle a unique name
|
||||
before you can proceed.
|
||||
</p>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<TextField
|
||||
@ -123,7 +140,11 @@ class CopyFeatureComponent extends Component {
|
||||
label="Replace groupId"
|
||||
/>
|
||||
|
||||
<Button type="submit" color="primary" variant="contained">
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
>
|
||||
<Icon>file_copy</Icon>
|
||||
Create from copy
|
||||
</Button>
|
||||
|
@ -3,7 +3,18 @@ import PropTypes from 'prop-types';
|
||||
import { Icon, Chip } from '@material-ui/core';
|
||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||
import Dialogue from '../common/Dialogue';
|
||||
function FeatureTagComponent({ tags, tagTypes, featureToggleName, untagFeature }) {
|
||||
|
||||
import slackIcon from '../../assets/icons/slack.svg';
|
||||
import jiraIcon from '../../assets/icons/jira.svg';
|
||||
import webhookIcon from '../../assets/icons/webhooks.svg';
|
||||
import { formatAssetPath } from '../../utils/format-path';
|
||||
|
||||
function FeatureTagComponent({
|
||||
tags,
|
||||
tagTypes,
|
||||
featureToggleName,
|
||||
untagFeature,
|
||||
}) {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [selectedTag, setSelectedTag] = useState(undefined);
|
||||
const onUntagFeature = tag => {
|
||||
@ -20,11 +31,29 @@ function FeatureTagComponent({ tags, tagTypes, featureToggleName, untagFeature }
|
||||
if (tagType && tagType.icon) {
|
||||
switch (tagType.name) {
|
||||
case 'slack':
|
||||
return <img style={style} alt="slack" src="slack.svg" />;
|
||||
return (
|
||||
<img
|
||||
style={style}
|
||||
alt="slack"
|
||||
src={formatAssetPath(slackIcon)}
|
||||
/>
|
||||
);
|
||||
case 'jira':
|
||||
return <img style={style} alt="jira" src="jira.svg" />;
|
||||
return (
|
||||
<img
|
||||
style={style}
|
||||
alt="jira"
|
||||
src={formatAssetPath(jiraIcon)}
|
||||
/>
|
||||
);
|
||||
case 'webhook':
|
||||
return <img style={style} alt="webhook" src="webhooks.svg" />;
|
||||
return (
|
||||
<img
|
||||
style={style}
|
||||
alt="webhook"
|
||||
src={formatAssetPath(webhookIcon)}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <Icon>label</Icon>;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import MySelect from '../common/select';
|
||||
class FeatureTypeSelectComponent extends Component {
|
||||
componentDidMount() {
|
||||
const { fetchFeatureTypes, types } = this.props;
|
||||
if (types[0].initial && fetchFeatureTypes) {
|
||||
if (types && types[0].initial && fetchFeatureTypes) {
|
||||
this.props.fetchFeatureTypes();
|
||||
}
|
||||
}
|
||||
@ -33,7 +33,17 @@ class FeatureTypeSelectComponent extends Component {
|
||||
options.push({ key: value, label: value });
|
||||
}
|
||||
|
||||
return <MySelect disabled={!editable} options={options} value={value} onChange={onChange} label={label} id={id} {...rest} />;
|
||||
return (
|
||||
<MySelect
|
||||
disabled={!editable}
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
label={label}
|
||||
id={id}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,11 @@ import { Provider } from 'react-redux';
|
||||
import { ThemeProvider } from '@material-ui/core';
|
||||
import ViewFeatureToggleComponent from '../../FeatureView/FeatureView';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { ADMIN, DELETE_FEATURE, UPDATE_FEATURE } from '../../../AccessProvider/permissions';
|
||||
import {
|
||||
ADMIN,
|
||||
DELETE_FEATURE,
|
||||
UPDATE_FEATURE,
|
||||
} from '../../../AccessProvider/permissions';
|
||||
|
||||
import theme from '../../../../themes/main-theme';
|
||||
import { createFakeStore } from '../../../../accessStoreFake';
|
||||
@ -63,18 +67,20 @@ test('renders correctly with one feature', () => {
|
||||
<MemoryRouter>
|
||||
<Provider store={createStore(mockReducer, mockStore)}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AccessProvider store={createFakeStore([{permission: ADMIN}])}>
|
||||
<ViewFeatureToggleComponent
|
||||
activeTab={'strategies'}
|
||||
featureToggleName="another"
|
||||
features={[feature]}
|
||||
featureToggle={feature}
|
||||
fetchFeatureToggles={jest.fn()}
|
||||
history={{}}
|
||||
featureTags={[]}
|
||||
fetchTags={jest.fn()}
|
||||
untagFeature={jest.fn()}
|
||||
/>
|
||||
<AccessProvider
|
||||
store={createFakeStore([{ permission: ADMIN }])}
|
||||
>
|
||||
<ViewFeatureToggleComponent
|
||||
activeTab={'strategies'}
|
||||
featureToggleName="another"
|
||||
features={[feature]}
|
||||
featureToggle={feature}
|
||||
fetchFeatureToggles={jest.fn()}
|
||||
history={{}}
|
||||
featureTags={[]}
|
||||
fetchTags={jest.fn()}
|
||||
untagFeature={jest.fn()}
|
||||
/>
|
||||
</AccessProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import MainLayout from '../MainLayout/MainLayout';
|
||||
import MainLayout from '../MainLayout';
|
||||
|
||||
const LayoutPicker = ({ children, location }) => {
|
||||
const standalonePages = () => {
|
||||
|
@ -8,6 +8,7 @@ import styles from '../../styles.module.scss';
|
||||
import ErrorContainer from '../../error/error-container';
|
||||
import Header from '../../menu/Header';
|
||||
import Footer from '../../menu/Footer/Footer';
|
||||
import Proclamation from '../../common/Proclamation/Proclamation';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
@ -16,7 +17,7 @@ const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const MainLayout = ({ children, location }) => {
|
||||
const MainLayout = ({ children, location, uiConfig }) => {
|
||||
const muiStyles = useStyles();
|
||||
|
||||
return (
|
||||
@ -26,6 +27,7 @@ const MainLayout = ({ children, location }) => {
|
||||
<div className={classnames(styles.contentWrapper)}>
|
||||
<Grid item className={styles.content} xs={12} sm={12}>
|
||||
<div className={styles.contentContainer}>
|
||||
<Proclamation toast={uiConfig.toast} />
|
||||
{children}
|
||||
</div>
|
||||
<ErrorContainer />
|
||||
|
10
frontend/src/component/layout/MainLayout/index.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { connect } from 'react-redux';
|
||||
import MainLayout from './MainLayout';
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
uiConfig: state.uiConfig.toJS(),
|
||||
location: ownProps.location,
|
||||
children: ownProps.children,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(MainLayout);
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||
import { useTheme } from '@material-ui/core/styles';
|
||||
@ -18,17 +18,12 @@ import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyR
|
||||
import MenuBookIcon from '@material-ui/icons/MenuBook';
|
||||
import { useStyles } from './styles';
|
||||
|
||||
const Header = ({ uiConfig, init }) => {
|
||||
const Header = ({ uiConfig }) => {
|
||||
const theme = useTheme();
|
||||
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const styles = useStyles();
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
init(uiConfig.flags);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const toggleDrawer = () => setOpenDrawer(prev => !prev);
|
||||
|
||||
const { links, name, flags } = uiConfig;
|
||||
@ -83,7 +78,6 @@ const Header = ({ uiConfig, init }) => {
|
||||
|
||||
Header.propTypes = {
|
||||
uiConfig: PropTypes.object.isRequired,
|
||||
init: PropTypes.func.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
@ -1,10 +1,7 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { loadInitialData } from '../../../store/loader';
|
||||
|
||||
import Header from './Header';
|
||||
|
||||
const mapStateToProps = state => ({ uiConfig: state.uiConfig.toJS() });
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
init: loadInitialData,
|
||||
})(Header);
|
||||
export default connect(mapStateToProps)(Header);
|
||||
|
@ -33,8 +33,12 @@ const renderRoute = (params, route) => {
|
||||
if (!route) {
|
||||
return null;
|
||||
}
|
||||
const title = route.title.startsWith(':') ? params[route.title.substring(1)] : route.title;
|
||||
return route.parent ? renderDoubleBread(title, getRoute(route.parent)) : renderBread(route);
|
||||
const title = route.title.startsWith(':')
|
||||
? params[route.title.substring(1)]
|
||||
: route.title;
|
||||
return route.parent
|
||||
? renderDoubleBread(title, getRoute(route.parent))
|
||||
: renderBread(route);
|
||||
};
|
||||
|
||||
/*
|
||||
@ -53,7 +57,9 @@ const Breadcrumb = () => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
render={({ match: { params } } = this.props) => renderRoute(params, route)}
|
||||
render={({ match: { params } } = this.props) =>
|
||||
renderRoute(params, route)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Switch>
|
||||
|
@ -8,6 +8,8 @@ import styles from './drawer.module.scss';
|
||||
|
||||
import { baseRoutes as routes } from './routes';
|
||||
|
||||
import logo from '../../assets/img/logo.png';
|
||||
|
||||
const filterByFlags = flags => r => {
|
||||
if (r.flag && !flags[r.flag]) {
|
||||
return false;
|
||||
@ -17,7 +19,15 @@ const filterByFlags = flags => r => {
|
||||
|
||||
function getIcon(name) {
|
||||
if (name === 'c_github') {
|
||||
return <i className={classnames('material-icons', styles.navigationIcon, styles.iconGitHub)} />;
|
||||
return (
|
||||
<i
|
||||
className={classnames(
|
||||
'material-icons',
|
||||
styles.navigationIcon,
|
||||
styles.iconGitHub
|
||||
)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <Icon className={styles.navigationIcon}>{name}</Icon>;
|
||||
}
|
||||
@ -43,7 +53,8 @@ function renderLink(link, toggleDrawer) {
|
||||
key={link.href}
|
||||
target="_blank"
|
||||
className={[styles.navigationLink].join(' ')}
|
||||
title={link.title} rel="noreferrer"
|
||||
title={link.title}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{getIcon(link.icon)} {link.value}
|
||||
</a>
|
||||
@ -51,12 +62,29 @@ function renderLink(link, toggleDrawer) {
|
||||
}
|
||||
}
|
||||
|
||||
export const DrawerMenu = ({ links = [], title = 'Unleash', flags = {}, open = false, toggleDrawer }) => (
|
||||
<Drawer className={styles.drawer} open={open} anchor={'left'} onClose={() => toggleDrawer()}>
|
||||
export const DrawerMenu = ({
|
||||
links = [],
|
||||
title = 'Unleash',
|
||||
flags = {},
|
||||
open = false,
|
||||
toggleDrawer,
|
||||
}) => (
|
||||
<Drawer
|
||||
className={styles.drawer}
|
||||
open={open}
|
||||
anchor={'left'}
|
||||
onClose={() => toggleDrawer()}
|
||||
>
|
||||
<div className={styles.drawerContainer}>
|
||||
<div className={styles.drawerTitleContainer}>
|
||||
<span className={[styles.drawerTitle].join(' ')}>
|
||||
<img alt="Unleash Logo" src="logo.png" width="32" height="32" className={styles.drawerTitleLogo} />
|
||||
<img
|
||||
alt="Unleash Logo"
|
||||
src={logo}
|
||||
width="32"
|
||||
height="32"
|
||||
className={styles.drawerTitleLogo}
|
||||
/>
|
||||
<span className={styles.drawerTitleText}>{title}</span>
|
||||
</span>
|
||||
</div>
|
||||
@ -68,7 +96,9 @@ export const DrawerMenu = ({ links = [], title = 'Unleash', flags = {}, open = f
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={classnames(styles.navigationLink)}
|
||||
activeClassName={classnames(styles.navigationLinkActive)}
|
||||
activeClassName={classnames(
|
||||
styles.navigationLinkActive
|
||||
)}
|
||||
>
|
||||
{getIcon(item.icon)}
|
||||
{item.title}
|
||||
@ -76,7 +106,9 @@ export const DrawerMenu = ({ links = [], title = 'Unleash', flags = {}, open = f
|
||||
))}
|
||||
</List>
|
||||
<Divider />
|
||||
<List className={styles.navigation}>{links.map(l => renderLink(l, toggleDrawer))}</List>
|
||||
<List className={styles.navigation}>
|
||||
{links.map(l => renderLink(l, toggleDrawer))}
|
||||
</List>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
|
@ -2,8 +2,20 @@ import PropTypes from 'prop-types';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import HeaderTitle from '../../common/HeaderTitle';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||
import { CREATE_PROJECT, DELETE_PROJECT, UPDATE_PROJECT } from '../../AccessProvider/permissions';
|
||||
import { Icon, IconButton, List, ListItem, ListItemAvatar, ListItemText, Tooltip } from '@material-ui/core';
|
||||
import {
|
||||
CREATE_PROJECT,
|
||||
DELETE_PROJECT,
|
||||
UPDATE_PROJECT,
|
||||
} from '../../AccessProvider/permissions';
|
||||
import {
|
||||
Icon,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
} from '@material-ui/core';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ConfirmDialogue from '../../common/Dialogue';
|
||||
import PageContent from '../../common/PageContent/PageContent';
|
||||
@ -24,7 +36,10 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history }) => {
|
||||
condition={hasAccess(CREATE_PROJECT)}
|
||||
show={
|
||||
<Tooltip title="Add new project">
|
||||
<IconButton aria-label="add-project" onClick={() => history.push('/projects/create')}>
|
||||
<IconButton
|
||||
aria-label="add-project"
|
||||
onClick={() => history.push('/projects/create')}
|
||||
>
|
||||
<Icon>add</Icon>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@ -40,8 +55,11 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history }) => {
|
||||
|
||||
const mgmAccessButton = project => (
|
||||
<Tooltip title="Manage access">
|
||||
<Link to={`/projects/${project.id}/access`} style={{ color: 'black' }}>
|
||||
<IconButton aria-label="manage_access" >
|
||||
<Link
|
||||
to={`/projects/${project.id}/access`}
|
||||
style={{ color: 'black' }}
|
||||
>
|
||||
<IconButton aria-label="manage_access">
|
||||
<Icon>supervised_user_circle</Icon>
|
||||
</IconButton>
|
||||
</Link>
|
||||
@ -68,17 +86,27 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history }) => {
|
||||
<ListItemAvatar>
|
||||
<Icon>folder_open</Icon>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={projectLink(project)} secondary={project.description} />
|
||||
<ListItemText
|
||||
primary={projectLink(project)}
|
||||
secondary={project.description}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(UPDATE_PROJECT, project.name)}
|
||||
show={mgmAccessButton(project)}
|
||||
/>
|
||||
<ConditionallyRender condition={hasAccess(DELETE_PROJECT, project.name)} show={deleteProjectButton(project)} />
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(DELETE_PROJECT, project.name)}
|
||||
show={deleteProjectButton(project)}
|
||||
/>
|
||||
</ListItem>
|
||||
));
|
||||
|
||||
return (
|
||||
<PageContent headerContent={<HeaderTitle title="Projects" actions={addProjectButton()} />}>
|
||||
<PageContent
|
||||
headerContent={
|
||||
<HeaderTitle title="Projects" actions={addProjectButton()} />
|
||||
}
|
||||
>
|
||||
<List>
|
||||
<ConditionallyRender
|
||||
condition={projects.length > 0}
|
||||
|
@ -42,15 +42,17 @@ test('renders correctly with one strategy without permissions', () => {
|
||||
const tree = renderer.create(
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AccessProvider store={createFakeStore([{permission: ADMIN}])}>
|
||||
<StrategiesListComponent
|
||||
strategies={[strategy]}
|
||||
fetchStrategies={jest.fn()}
|
||||
removeStrategy={jest.fn()}
|
||||
deprecateStrategy={jest.fn()}
|
||||
reactivateStrategy={jest.fn()}
|
||||
history={{}}
|
||||
/>
|
||||
<AccessProvider
|
||||
store={createFakeStore([{ permission: ADMIN }])}
|
||||
>
|
||||
<StrategiesListComponent
|
||||
strategies={[strategy]}
|
||||
fetchStrategies={jest.fn()}
|
||||
removeStrategy={jest.fn()}
|
||||
deprecateStrategy={jest.fn()}
|
||||
reactivateStrategy={jest.fn()}
|
||||
history={{}}
|
||||
/>
|
||||
</AccessProvider>
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
|
@ -36,7 +36,7 @@ test('renders correctly with one strategy', () => {
|
||||
const tree = renderer.create(
|
||||
<MemoryRouter>
|
||||
<AccessProvider store={createFakeStore()}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<StrategyDetails
|
||||
strategyName={'Another'}
|
||||
strategy={strategy}
|
||||
@ -48,7 +48,7 @@ test('renders correctly with one strategy', () => {
|
||||
fetchFeatureToggles={jest.fn()}
|
||||
history={{}}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</ThemeProvider>
|
||||
</AccessProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
@ -51,7 +51,13 @@ export default class StrategyDetails extends Component {
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
component: <EditStrategy strategy={this.props.strategy} history={this.props.history} editMode />,
|
||||
component: (
|
||||
<EditStrategy
|
||||
strategy={this.props.strategy}
|
||||
history={this.props.history}
|
||||
editMode
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@ -61,9 +67,13 @@ export default class StrategyDetails extends Component {
|
||||
<PageContent headerContent={strategy.name}>
|
||||
<Grid container>
|
||||
<Grid item xs={12} sm={12}>
|
||||
<Typography variant="subtitle1">{strategy.description}</Typography>
|
||||
<Typography variant="subtitle1">
|
||||
{strategy.description}
|
||||
</Typography>
|
||||
<ConditionallyRender
|
||||
condition={strategy.editable && hasAccess(UPDATE_STRATEGY)}
|
||||
condition={
|
||||
strategy.editable && hasAccess(UPDATE_STRATEGY)
|
||||
}
|
||||
show={
|
||||
<div>
|
||||
<TabNav tabData={tabData} />
|
||||
@ -75,7 +85,9 @@ export default class StrategyDetails extends Component {
|
||||
<ShowStrategy
|
||||
strategy={this.props.strategy}
|
||||
toggles={this.props.toggles}
|
||||
applications={this.props.applications}
|
||||
applications={
|
||||
this.props.applications
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -16,7 +16,10 @@ import {
|
||||
import HeaderTitle from '../../common/HeaderTitle';
|
||||
import PageContent from '../../common/PageContent/PageContent';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||
import { CREATE_TAG_TYPE, DELETE_TAG_TYPE } from '../../AccessProvider/permissions';
|
||||
import {
|
||||
CREATE_TAG_TYPE,
|
||||
DELETE_TAG_TYPE,
|
||||
} from '../../AccessProvider/permissions';
|
||||
import Dialogue from '../../common/Dialogue/Dialogue';
|
||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||
|
||||
@ -28,7 +31,6 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
|
||||
const [deletion, setDeletion] = useState({ open: false });
|
||||
const history = useHistory();
|
||||
const smallScreen = useMediaQuery('(max-width:700px)');
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchTagTypes();
|
||||
@ -48,7 +50,9 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
|
||||
<Tooltip title="Add tag type">
|
||||
<IconButton
|
||||
aria-label="add tag type"
|
||||
onClick={() => history.push('/tag-types/create')}
|
||||
onClick={() =>
|
||||
history.push('/tag-types/create')
|
||||
}
|
||||
>
|
||||
<Icon>add</Icon>
|
||||
</IconButton>
|
||||
@ -58,7 +62,9 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => history.push('/tag-types/create')}
|
||||
onClick={() =>
|
||||
history.push('/tag-types/create')
|
||||
}
|
||||
>
|
||||
Add new tag type
|
||||
</Button>
|
||||
@ -91,12 +97,18 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
|
||||
</Tooltip>
|
||||
);
|
||||
return (
|
||||
<ListItem key={`${tagType.name}`} classes={{ root: styles.tagListItem }}>
|
||||
<ListItem
|
||||
key={`${tagType.name}`}
|
||||
classes={{ root: styles.tagListItem }}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Icon>label</Icon>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={link} secondary={tagType.description} />
|
||||
<ConditionallyRender condition={hasAccess(DELETE_TAG_TYPE)} show={deleteButton} />
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(DELETE_TAG_TYPE)}
|
||||
show={deleteButton}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
@ -5,7 +5,10 @@ import renderer from 'react-test-renderer';
|
||||
import theme from '../../../themes/main-theme';
|
||||
import AccessProvider from '../../AccessProvider/AccessProvider';
|
||||
import { createFakeStore } from '../../../accessStoreFake';
|
||||
import { CREATE_TAG_TYPE, UPDATE_TAG_TYPE } from '../../AccessProvider/permissions';
|
||||
import {
|
||||
CREATE_TAG_TYPE,
|
||||
UPDATE_TAG_TYPE,
|
||||
} from '../../AccessProvider/permissions';
|
||||
|
||||
jest.mock('@material-ui/core/TextField');
|
||||
|
||||
@ -13,16 +16,18 @@ test('renders correctly for creating', () => {
|
||||
const tree = renderer
|
||||
.create(
|
||||
<ThemeProvider theme={theme}>
|
||||
<AccessProvider store={createFakeStore([{permission: CREATE_TAG_TYPE}])}>
|
||||
<TagTypes
|
||||
history={{}}
|
||||
title="Add tag type"
|
||||
createTagType={jest.fn()}
|
||||
validateName={() => Promise.resolve(true)}
|
||||
tagType={{ name: '', description: '', icon: '' }}
|
||||
editMode={false}
|
||||
submit={jest.fn()}
|
||||
/>
|
||||
<AccessProvider
|
||||
store={createFakeStore([{ permission: CREATE_TAG_TYPE }])}
|
||||
>
|
||||
<TagTypes
|
||||
history={{}}
|
||||
title="Add tag type"
|
||||
createTagType={jest.fn()}
|
||||
validateName={() => Promise.resolve(true)}
|
||||
tagType={{ name: '', description: '', icon: '' }}
|
||||
editMode={false}
|
||||
submit={jest.fn()}
|
||||
/>
|
||||
</AccessProvider>
|
||||
</ThemeProvider>
|
||||
)
|
||||
@ -35,15 +40,15 @@ test('renders correctly for creating without permissions', () => {
|
||||
.create(
|
||||
<ThemeProvider theme={theme}>
|
||||
<AccessProvider store={createFakeStore([])}>
|
||||
<TagTypes
|
||||
history={{}}
|
||||
title="Add tag type"
|
||||
createTagType={jest.fn()}
|
||||
validateName={() => Promise.resolve(true)}
|
||||
tagType={{ name: '', description: '', icon: '' }}
|
||||
editMode={false}
|
||||
submit={jest.fn()}
|
||||
/>
|
||||
<TagTypes
|
||||
history={{}}
|
||||
title="Add tag type"
|
||||
createTagType={jest.fn()}
|
||||
validateName={() => Promise.resolve(true)}
|
||||
tagType={{ name: '', description: '', icon: '' }}
|
||||
editMode={false}
|
||||
submit={jest.fn()}
|
||||
/>
|
||||
</AccessProvider>
|
||||
</ThemeProvider>
|
||||
)
|
||||
@ -55,16 +60,18 @@ test('it supports editMode', () => {
|
||||
const tree = renderer
|
||||
.create(
|
||||
<ThemeProvider theme={theme}>
|
||||
<AccessProvider store={createFakeStore([{permission: UPDATE_TAG_TYPE}])}>
|
||||
<TagTypes
|
||||
history={{}}
|
||||
title="Add tag type"
|
||||
createTagType={jest.fn()}
|
||||
validateName={() => Promise.resolve(true)}
|
||||
tagType={{ name: '', description: '', icon: '' }}
|
||||
editMode
|
||||
submit={jest.fn()}
|
||||
/>
|
||||
<AccessProvider
|
||||
store={createFakeStore([{ permission: UPDATE_TAG_TYPE }])}
|
||||
>
|
||||
<TagTypes
|
||||
history={{}}
|
||||
title="Add tag type"
|
||||
createTagType={jest.fn()}
|
||||
validateName={() => Promise.resolve(true)}
|
||||
tagType={{ name: '', description: '', icon: '' }}
|
||||
editMode
|
||||
submit={jest.fn()}
|
||||
/>
|
||||
</AccessProvider>
|
||||
</ThemeProvider>
|
||||
)
|
||||
|
@ -8,15 +8,20 @@ import theme from '../../../themes/main-theme';
|
||||
import { createFakeStore } from '../../../accessStoreFake';
|
||||
import AccessProvider from '../../AccessProvider/AccessProvider';
|
||||
|
||||
import { ADMIN, CREATE_TAG_TYPE, UPDATE_TAG_TYPE, DELETE_TAG_TYPE } from '../../AccessProvider/permissions';
|
||||
|
||||
|
||||
import {
|
||||
ADMIN,
|
||||
CREATE_TAG_TYPE,
|
||||
UPDATE_TAG_TYPE,
|
||||
DELETE_TAG_TYPE,
|
||||
} from '../../AccessProvider/permissions';
|
||||
|
||||
test('renders an empty list correctly', () => {
|
||||
const tree = renderer.create(
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AccessProvider store={createFakeStore([{permission: ADMIN }])}>
|
||||
<AccessProvider
|
||||
store={createFakeStore([{ permission: ADMIN }])}
|
||||
>
|
||||
<TagTypesList
|
||||
tagTypes={[]}
|
||||
fetchTagTypes={jest.fn()}
|
||||
@ -34,11 +39,13 @@ test('renders a list with elements correctly', () => {
|
||||
const tree = renderer.create(
|
||||
<ThemeProvider theme={theme}>
|
||||
<MemoryRouter>
|
||||
<AccessProvider store={createFakeStore([
|
||||
{permission: CREATE_TAG_TYPE },
|
||||
{permission: UPDATE_TAG_TYPE },
|
||||
{permission: DELETE_TAG_TYPE }
|
||||
])}>
|
||||
<AccessProvider
|
||||
store={createFakeStore([
|
||||
{ permission: CREATE_TAG_TYPE },
|
||||
{ permission: UPDATE_TAG_TYPE },
|
||||
{ permission: DELETE_TAG_TYPE },
|
||||
])}
|
||||
>
|
||||
<TagTypesList
|
||||
tagTypes={[
|
||||
{
|
||||
|
@ -9,12 +9,23 @@ import { Typography, TextField } from '@material-ui/core';
|
||||
import styles from './TagType.module.scss';
|
||||
import commonStyles from '../common/common.module.scss';
|
||||
import AccessContext from '../../contexts/AccessContext';
|
||||
import { CREATE_TAG_TYPE, UPDATE_TAG_TYPE } from '../AccessProvider/permissions';
|
||||
import {
|
||||
CREATE_TAG_TYPE,
|
||||
UPDATE_TAG_TYPE,
|
||||
} from '../AccessProvider/permissions';
|
||||
import ConditionallyRender from '../common/ConditionallyRender';
|
||||
|
||||
const AddTagTypeComponent = ({ tagType, validateName, submit, history, editMode }) => {
|
||||
const AddTagTypeComponent = ({
|
||||
tagType,
|
||||
validateName,
|
||||
submit,
|
||||
history,
|
||||
editMode,
|
||||
}) => {
|
||||
const [tagTypeName, setTagTypeName] = useState(tagType.name || '');
|
||||
const [tagTypeDescription, setTagTypeDescription] = useState(tagType.description || '');
|
||||
const [tagTypeDescription, setTagTypeDescription] = useState(
|
||||
tagType.description || ''
|
||||
);
|
||||
const [errors, setErrors] = useState({
|
||||
general: undefined,
|
||||
name: undefined,
|
||||
@ -53,11 +64,23 @@ const AddTagTypeComponent = ({ tagType, validateName, submit, history, editMode
|
||||
const submitText = editMode ? 'Update' : 'Create';
|
||||
return (
|
||||
<PageContent headerContent={`${submitText} Tag type`}>
|
||||
<section className={classnames(commonStyles.contentSpacing, styles.tagTypeContainer)}>
|
||||
<section
|
||||
className={classnames(
|
||||
commonStyles.contentSpacing,
|
||||
styles.tagTypeContainer
|
||||
)}
|
||||
>
|
||||
<Typography variant="subtitle1">
|
||||
Tag types allows you to group tags together in the management UI
|
||||
Tag types allows you to group tags together in the
|
||||
management UI
|
||||
</Typography>
|
||||
<form onSubmit={onSubmit} className={classnames(styles.addTagTypeForm, commonStyles.contentSpacing)}>
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className={classnames(
|
||||
styles.addTagTypeForm,
|
||||
commonStyles.contentSpacing
|
||||
)}
|
||||
>
|
||||
<TextField
|
||||
label="Name"
|
||||
name="name"
|
||||
@ -84,11 +107,22 @@ const AddTagTypeComponent = ({ tagType, validateName, submit, history, editMode
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
<ConditionallyRender condition={hasAccess(editMode ? UPDATE_TAG_TYPE : CREATE_TAG_TYPE)} show={
|
||||
<div className={styles.formButtons}>
|
||||
<FormButtons submitText={submitText} onCancel={onCancel} />
|
||||
</div>
|
||||
} elseShow={<span>You do not have permissions to save.</span>} />
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(
|
||||
editMode ? UPDATE_TAG_TYPE : CREATE_TAG_TYPE
|
||||
)}
|
||||
show={
|
||||
<div className={styles.formButtons}>
|
||||
<FormButtons
|
||||
submitText={submitText}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
elseShow={
|
||||
<span>You do not have permissions to save.</span>
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
</section>
|
||||
</PageContent>
|
||||
|
@ -3,7 +3,16 @@ import PropTypes from 'prop-types';
|
||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { Button, Icon, IconButton, List, ListItem, ListItemIcon, ListItemText, Tooltip } from '@material-ui/core';
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
} from '@material-ui/core';
|
||||
import { CREATE_TAG, DELETE_TAG } from '../../AccessProvider/permissions';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||
import HeaderTitle from '../../common/HeaderTitle';
|
||||
@ -29,7 +38,10 @@ const TagList = ({ tags, fetchTags, removeTag }) => {
|
||||
};
|
||||
|
||||
const listItem = tag => (
|
||||
<ListItem key={`${tag.type}_${tag.value}`} className={styles.tagListItem}>
|
||||
<ListItem
|
||||
key={`${tag.type}_${tag.value}`}
|
||||
className={styles.tagListItem}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Icon>label</Icon>
|
||||
</ListItemIcon>
|
||||
@ -43,7 +55,9 @@ const TagList = ({ tags, fetchTags, removeTag }) => {
|
||||
|
||||
const DeleteButton = ({ tagType, tagValue }) => (
|
||||
<Tooltip title="Delete tag">
|
||||
<IconButton onClick={e => remove({ type: tagType, value: tagValue }, e)}>
|
||||
<IconButton
|
||||
onClick={e => remove({ type: tagType, value: tagValue }, e)}
|
||||
>
|
||||
<Icon>delete</Icon>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@ -61,7 +75,10 @@ const TagList = ({ tags, fetchTags, removeTag }) => {
|
||||
<ConditionallyRender
|
||||
condition={smallScreen}
|
||||
show={
|
||||
<IconButton aria-label="add tag" onClick={() => history.push('/tags/create')}>
|
||||
<IconButton
|
||||
aria-label="add tag"
|
||||
onClick={() => history.push('/tags/create')}
|
||||
>
|
||||
<Icon>add</Icon>
|
||||
</IconButton>
|
||||
}
|
||||
@ -82,7 +99,14 @@ const TagList = ({ tags, fetchTags, removeTag }) => {
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<PageContent headerContent={<HeaderTitle title="Tags" actions={<AddButton hasAccess={hasAccess} />} />}>
|
||||
<PageContent
|
||||
headerContent={
|
||||
<HeaderTitle
|
||||
title="Tags"
|
||||
actions={<AddButton hasAccess={hasAccess} />}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<List>
|
||||
<ConditionallyRender
|
||||
condition={tags.length > 0}
|
||||
|
@ -4,12 +4,9 @@ import { Button, TextField } from '@material-ui/core';
|
||||
|
||||
import styles from './DemoAuth.module.scss';
|
||||
|
||||
const DemoAuth = ({
|
||||
demoLogin,
|
||||
loadInitialData,
|
||||
history,
|
||||
authDetails,
|
||||
}) => {
|
||||
import logoIcon from '../../../assets/img/logo.png';
|
||||
|
||||
const DemoAuth = ({ demoLogin, history, authDetails }) => {
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const handleSubmit = evt => {
|
||||
@ -17,9 +14,7 @@ const DemoAuth = ({
|
||||
const user = { email };
|
||||
const path = evt.target.action;
|
||||
|
||||
demoLogin(path, user)
|
||||
.then(loadInitialData)
|
||||
.then(() => history.push(`/`));
|
||||
demoLogin(path, user).then(() => history.push(`/`));
|
||||
};
|
||||
|
||||
const handleChange = e => {
|
||||
@ -30,7 +25,7 @@ const DemoAuth = ({
|
||||
return (
|
||||
<form onSubmit={handleSubmit} action={authDetails.path}>
|
||||
<div className={styles.container}>
|
||||
<img alt="Unleash Logo" src="logo.png" width="70" height="70" />
|
||||
<img alt="Unleash Logo" src={logoIcon} width="70" height="70" />
|
||||
<h2>Access the Unleash demo instance</h2>
|
||||
<p>No further data or Credit Card required</p>
|
||||
<div className={styles.form}>
|
||||
@ -46,7 +41,6 @@ const DemoAuth = ({
|
||||
type="email"
|
||||
/>
|
||||
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
@ -58,16 +52,23 @@ const DemoAuth = ({
|
||||
</Button>
|
||||
</div>
|
||||
<p>
|
||||
By accessing our demo instance, you agree to the Unleash
|
||||
By accessing our demo instance, you agree to the
|
||||
Unleash
|
||||
<a
|
||||
href="https://www.unleash-hosted.com/tos/"
|
||||
target="_blank" rel="noreferrer">
|
||||
Customer Terms of Service
|
||||
</a> and
|
||||
<a href="https://www.unleash-hosted.com/privacy-policy/" target="_blank" rel="noreferrer">
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Customer Terms of Service
|
||||
</a>{' '}
|
||||
and
|
||||
<a
|
||||
href="https://www.unleash-hosted.com/privacy-policy/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
@ -77,7 +78,6 @@ const DemoAuth = ({
|
||||
DemoAuth.propTypes = {
|
||||
authDetails: PropTypes.object.isRequired,
|
||||
demoLogin: PropTypes.func.isRequired,
|
||||
loadInitialData: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
@ -4,6 +4,7 @@ import classnames from 'classnames';
|
||||
import { SyntheticEvent, useState } from 'react';
|
||||
import { useCommonStyles } from '../../../common.styles';
|
||||
import useLoading from '../../../hooks/useLoading';
|
||||
import { formatApiPath } from '../../../utils/format-path';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
|
||||
import { useStyles } from './ForgottenPassword.styles';
|
||||
@ -22,7 +23,8 @@ const ForgottenPassword = () => {
|
||||
setLoading(true);
|
||||
setAttemptedEmail(email);
|
||||
|
||||
await fetch('auth/reset/password-email', {
|
||||
const path = formatApiPath('auth/reset/password-email');
|
||||
await fetch(path, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
@ -8,17 +8,10 @@ import useQueryParams from '../../../hooks/useQueryParams';
|
||||
import ResetPasswordSuccess from '../common/ResetPasswordSuccess/ResetPasswordSuccess';
|
||||
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
|
||||
|
||||
const Login = ({ history, loadInitialData, isUnauthorized, authDetails }) => {
|
||||
const Login = ({ history, isUnauthorized, authDetails }) => {
|
||||
const styles = useStyles();
|
||||
const query = useQueryParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (isUnauthorized()) {
|
||||
loadInitialData();
|
||||
}
|
||||
/* eslint-disable-next-line */
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isUnauthorized()) {
|
||||
history.push('features');
|
||||
|
@ -1,14 +1,9 @@
|
||||
import { connect } from 'react-redux';
|
||||
import Login from './Login';
|
||||
import { loadInitialData } from './../../../store/loader';
|
||||
|
||||
const mapDispatchToProps = (dispatch, props) => ({
|
||||
loadInitialData: () => loadInitialData(props.flags)(dispatch),
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
user: state.user.toJS(),
|
||||
flags: state.uiConfig.toJS().flags,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Login);
|
||||
export default connect(mapStateToProps)(Login);
|
||||
|
@ -8,7 +8,7 @@ import { useCommonStyles } from '../../../common.styles';
|
||||
import { useStyles } from './PasswordAuth.styles';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const PasswordAuth = ({ authDetails, passwordLogin, loadInitialData }) => {
|
||||
const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
||||
const commonStyles = useCommonStyles();
|
||||
const styles = useStyles();
|
||||
const history = useHistory();
|
||||
@ -50,7 +50,7 @@ const PasswordAuth = ({ authDetails, passwordLogin, loadInitialData }) => {
|
||||
|
||||
try {
|
||||
await passwordLogin(path, user);
|
||||
await loadInitialData();
|
||||
|
||||
history.push(`/`);
|
||||
} catch (error) {
|
||||
if (error.statusCode === 404 || error.statusCode === 400) {
|
||||
@ -168,7 +168,6 @@ const PasswordAuth = ({ authDetails, passwordLogin, loadInitialData }) => {
|
||||
PasswordAuth.propTypes = {
|
||||
authDetails: PropTypes.object.isRequired,
|
||||
passwordLogin: PropTypes.func.isRequired,
|
||||
loadInitialData: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
@ -4,12 +4,7 @@ import { Button, TextField } from '@material-ui/core';
|
||||
|
||||
import styles from './SimpleAuth.module.scss';
|
||||
|
||||
const SimpleAuth = ({
|
||||
insecureLogin,
|
||||
loadInitialData,
|
||||
history,
|
||||
authDetails,
|
||||
}) => {
|
||||
const SimpleAuth = ({ insecureLogin, history, authDetails }) => {
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const handleSubmit = evt => {
|
||||
@ -17,9 +12,7 @@ const SimpleAuth = ({
|
||||
const user = { email };
|
||||
const path = evt.target.action;
|
||||
|
||||
insecureLogin(path, user)
|
||||
.then(loadInitialData)
|
||||
.then(() => history.push(`/`));
|
||||
insecureLogin(path, user).then(() => history.push(`/`));
|
||||
};
|
||||
|
||||
const handleChange = e => {
|
||||
@ -74,7 +67,6 @@ const SimpleAuth = ({
|
||||
SimpleAuth.propTypes = {
|
||||
authDetails: PropTypes.object.isRequired,
|
||||
insecureLogin: PropTypes.func.isRequired,
|
||||
loadInitialData: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
@ -2,9 +2,9 @@ import { FC } from 'react';
|
||||
|
||||
import { Typography, useTheme } from '@material-ui/core';
|
||||
import Gradient from '../../common/Gradient/Gradient';
|
||||
import { ReactComponent as StarIcon } from '../../../icons/star.svg';
|
||||
import { ReactComponent as RightToggleIcon } from '../../../icons/toggleRight.svg';
|
||||
import { ReactComponent as LeftToggleIcon } from '../../../icons/toggleLeft.svg';
|
||||
import { ReactComponent as StarIcon } from '../../../assets/icons/star.svg';
|
||||
import { ReactComponent as RightToggleIcon } from '../../../assets/icons/toggleRight.svg';
|
||||
import { ReactComponent as LeftToggleIcon } from '../../../assets/icons/toggleLeft.svg';
|
||||
|
||||
import { useStyles } from './StandaloneBanner.styles';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
OK,
|
||||
UNAUTHORIZED,
|
||||
} from '../../../../constants/statusCodes';
|
||||
import { formatApiPath } from '../../../../utils/format-path';
|
||||
|
||||
interface IEditProfileProps {
|
||||
setEditingProfile: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
@ -45,7 +46,8 @@ const EditProfile = ({
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await fetch('api/admin/user/change-password', {
|
||||
const path = formatApiPath('api/admin/user/change-password');
|
||||
const res = await fetch(path, {
|
||||
headers,
|
||||
body: JSON.stringify({ password, confirmPassword }),
|
||||
method: 'POST',
|
||||
|
@ -37,7 +37,6 @@ const UserProfile = ({
|
||||
|
||||
useEffect(() => {
|
||||
fetchUser();
|
||||
|
||||
const locale = navigator.language || navigator.userLanguage;
|
||||
let found = possibleLocales.find(l =>
|
||||
l.toLowerCase().includes(locale.toLowerCase())
|
||||
|
@ -24,7 +24,6 @@ const UserProfileContent = ({
|
||||
imageUrl,
|
||||
currentLocale,
|
||||
setCurrentLocale,
|
||||
location,
|
||||
logoutUser,
|
||||
}) => {
|
||||
const commonStyles = useCommonStyles();
|
||||
|
@ -17,7 +17,6 @@ class AuthComponent extends React.Component {
|
||||
demoLogin: PropTypes.func.isRequired,
|
||||
insecureLogin: PropTypes.func.isRequired,
|
||||
passwordLogin: PropTypes.func.isRequired,
|
||||
loadInitialData: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@ -31,7 +30,6 @@ class AuthComponent extends React.Component {
|
||||
<PasswordAuth
|
||||
passwordLogin={this.props.passwordLogin}
|
||||
authDetails={authDetails}
|
||||
loadInitialData={this.props.loadInitialData}
|
||||
history={this.props.history}
|
||||
/>
|
||||
);
|
||||
@ -40,7 +38,6 @@ class AuthComponent extends React.Component {
|
||||
<SimpleAuth
|
||||
insecureLogin={this.props.insecureLogin}
|
||||
authDetails={authDetails}
|
||||
loadInitialData={this.props.loadInitialData}
|
||||
history={this.props.history}
|
||||
/>
|
||||
);
|
||||
@ -49,7 +46,6 @@ class AuthComponent extends React.Component {
|
||||
<DemoAuth
|
||||
demoLogin={this.props.demoLogin}
|
||||
authDetails={authDetails}
|
||||
loadInitialData={this.props.loadInitialData}
|
||||
history={this.props.history}
|
||||
/>
|
||||
);
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { connect } from 'react-redux';
|
||||
import AuthenticationComponent from './authentication-component';
|
||||
import { insecureLogin, passwordLogin, demoLogin } from '../../store/user/actions';
|
||||
import { loadInitialData } from './../../store/loader';
|
||||
import {
|
||||
insecureLogin,
|
||||
passwordLogin,
|
||||
demoLogin,
|
||||
} from '../../store/user/actions';
|
||||
|
||||
const mapDispatchToProps = (dispatch, props) => ({
|
||||
demoLogin: (path, user) => demoLogin(path, user)(dispatch),
|
||||
insecureLogin: (path, user) => insecureLogin(path, user)(dispatch),
|
||||
passwordLogin: (path, user) => passwordLogin(path, user)(dispatch),
|
||||
loadInitialData: () => loadInitialData(props.flags)(dispatch),
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
@ -15,4 +17,7 @@ const mapStateToProps = state => ({
|
||||
flags: state.uiConfig.toJS().flags,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AuthenticationComponent);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(AuthenticationComponent);
|
||||
|
@ -5,6 +5,7 @@ import { BAD_REQUEST, OK } from '../../../../../constants/statusCodes';
|
||||
import { useStyles } from './PasswordChecker.styles';
|
||||
import HelpIcon from '@material-ui/icons/Help';
|
||||
import { useCallback } from 'react';
|
||||
import { formatApiPath } from '../../../../../utils/format-path';
|
||||
|
||||
interface IPasswordCheckerProps {
|
||||
password: string;
|
||||
@ -37,7 +38,8 @@ const PasswordChecker = ({ password, callback }: IPasswordCheckerProps) => {
|
||||
const [lengthError, setLengthError] = useState(true);
|
||||
|
||||
const makeValidatePassReq = useCallback(() => {
|
||||
return fetch('auth/reset/validate-password', {
|
||||
const path = formatApiPath('auth/reset/validate-password');
|
||||
return fetch(path, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@ -63,7 +65,7 @@ const PasswordChecker = ({ password, callback }: IPasswordCheckerProps) => {
|
||||
}
|
||||
} catch (e) {
|
||||
// ResetPasswordForm handles errors related to submitting the form.
|
||||
console.log(e);
|
||||
console.log('An exception was caught and handled');
|
||||
}
|
||||
}, [makeValidatePassReq, callback, password]);
|
||||
|
||||
|
@ -16,6 +16,7 @@ import PasswordChecker from './PasswordChecker/PasswordChecker';
|
||||
import PasswordMatcher from './PasswordMatcher/PasswordMatcher';
|
||||
import { useStyles } from './ResetPasswordForm.styles';
|
||||
import { useCallback } from 'react';
|
||||
import { formatApiPath } from '../../../../utils/format-path';
|
||||
|
||||
interface IResetPasswordProps {
|
||||
token: string;
|
||||
@ -47,7 +48,8 @@ const ResetPasswordForm = ({ token, setLoading }: IResetPasswordProps) => {
|
||||
}, [password, confirmPassword]);
|
||||
|
||||
const makeResetPasswordReq = () => {
|
||||
return fetch('auth/reset/password', {
|
||||
const path = formatApiPath('auth/reset/password');
|
||||
return fetch(path, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
headers,
|
||||
NotFoundError,
|
||||
} from '../store/api-helper';
|
||||
import { formatApiPath } from '../utils/format-path';
|
||||
|
||||
export interface IUserApiErrors {
|
||||
addUser?: string;
|
||||
@ -62,7 +63,8 @@ const useAdminUsersApi = () => {
|
||||
|
||||
const addUser = async (user: IUserPayload) => {
|
||||
return makeRequest(() => {
|
||||
return fetch('api/admin/user-admin', {
|
||||
const path = formatApiPath('api/admin/user-admin');
|
||||
return fetch(path, {
|
||||
...defaultOptions,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(user),
|
||||
@ -72,7 +74,8 @@ const useAdminUsersApi = () => {
|
||||
|
||||
const removeUser = async (user: IUserPayload) => {
|
||||
return makeRequest(() => {
|
||||
return fetch(`api/admin/user-admin/${user.id}`, {
|
||||
const path = formatApiPath(`api/admin/user-admin/${user.id}`);
|
||||
return fetch(path, {
|
||||
...defaultOptions,
|
||||
method: 'DELETE',
|
||||
});
|
||||
@ -81,7 +84,9 @@ const useAdminUsersApi = () => {
|
||||
|
||||
const updateUser = async (user: IUserPayload) => {
|
||||
return makeRequest(() => {
|
||||
return fetch(`api/admin/user-admin/${user.id}`, {
|
||||
const path = formatApiPath(`api/admin/user-admin/${user.id}`);
|
||||
|
||||
return fetch(path, {
|
||||
...defaultOptions,
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(user),
|
||||
@ -91,7 +96,10 @@ const useAdminUsersApi = () => {
|
||||
|
||||
const changePassword = async (user: IUserPayload, password: string) => {
|
||||
return makeRequest(() => {
|
||||
return fetch(`api/admin/user-admin/${user.id}/change-password`, {
|
||||
const path = formatApiPath(
|
||||
`api/admin/user-admin/${user.id}/change-password`
|
||||
);
|
||||
return fetch(path, {
|
||||
...defaultOptions,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ password }),
|
||||
@ -101,7 +109,10 @@ const useAdminUsersApi = () => {
|
||||
|
||||
const validatePassword = async (password: string) => {
|
||||
return makeRequest(() => {
|
||||
return fetch(`api/admin/user-admin/validate-password`, {
|
||||
const path = formatApiPath(
|
||||
`api/admin/user-admin/validate-password`
|
||||
);
|
||||
return fetch(path, {
|
||||
...defaultOptions,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ password }),
|
||||
|
@ -1,11 +1,14 @@
|
||||
import useSWR from 'swr';
|
||||
import useQueryParams from './useQueryParams';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { formatApiPath } from '../utils/format-path';
|
||||
|
||||
const getFetcher = (token: string) => () =>
|
||||
fetch(`auth/reset/validate?token=${token}`, {
|
||||
const getFetcher = (token: string) => () => {
|
||||
const path = formatApiPath(`auth/reset/validate?token=${token}`);
|
||||
return fetch(path, {
|
||||
method: 'GET',
|
||||
}).then(res => res.json());
|
||||
};
|
||||
|
||||
const INVALID_TOKEN_ERROR = 'InvalidTokenError';
|
||||
const USED_TOKEN_ERROR = 'UsedTokenError';
|
||||
@ -16,10 +19,9 @@ const useResetPassword = () => {
|
||||
const [token, setToken] = useState(initialToken);
|
||||
|
||||
const fetcher = getFetcher(token);
|
||||
const { data, error } = useSWR(
|
||||
`auth/reset/validate?token=${token}`,
|
||||
fetcher
|
||||
);
|
||||
|
||||
const key = `auth/reset/validate?token=${token}`;
|
||||
const { data, error } = useSWR(key, fetcher);
|
||||
const [loading, setLoading] = useState(!error && !data);
|
||||
|
||||
const retry = () => {
|
||||
|
@ -1,11 +1,14 @@
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { formatApiPath } from '../utils/format-path';
|
||||
|
||||
const useUsers = () => {
|
||||
const fetcher = () =>
|
||||
fetch(`api/admin/user-admin`, {
|
||||
const fetcher = () => {
|
||||
const path = formatApiPath(`api/admin/user-admin`);
|
||||
return fetch(path, {
|
||||
method: 'GET',
|
||||
}).then(res => res.json());
|
||||
};
|
||||
|
||||
const { data, error } = useSWR(`api/admin/user-admin`, fetcher);
|
||||
const [loading, setLoading] = useState(!error && !data);
|
||||
|
@ -3,7 +3,7 @@ import 'whatwg-fetch';
|
||||
import './app.css';
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import { HashRouter, Route } from 'react-router-dom';
|
||||
import { Route, BrowserRouter as Router } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { ThemeProvider, CssBaseline } from '@material-ui/core';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
@ -15,10 +15,11 @@ import { StylesProvider } from '@material-ui/core/styles';
|
||||
import mainTheme from './themes/main-theme';
|
||||
import store from './store';
|
||||
import MetricsPoller from './metrics-poller';
|
||||
import App from './component/App';
|
||||
import App from './component/AppContainer';
|
||||
import ScrollToTop from './component/scroll-to-top';
|
||||
import { writeWarning } from './security-logger';
|
||||
import AccessProvider from './component/AccessProvider/AccessProvider';
|
||||
import { getBasePath } from './utils/format-path';
|
||||
|
||||
let composeEnhancers;
|
||||
|
||||
@ -43,7 +44,7 @@ ReactDOM.render(
|
||||
<Provider store={unleashStore}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<AccessProvider store={unleashStore}>
|
||||
<HashRouter>
|
||||
<Router basename={`${getBasePath()}`}>
|
||||
<ThemeProvider theme={mainTheme}>
|
||||
<StylesProvider injectFirst>
|
||||
<CssBaseline />
|
||||
@ -52,7 +53,7 @@ ReactDOM.render(
|
||||
</ScrollToTop>
|
||||
</StylesProvider>
|
||||
</ThemeProvider>
|
||||
</HashRouter>
|
||||
</Router>
|
||||
</AccessProvider>
|
||||
</DndProvider>
|
||||
</Provider>,
|
||||
|
@ -1,7 +1,15 @@
|
||||
/* eslint-disable no-alert */
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon, Table, TableHead, TableBody, TableRow, TableCell, IconButton } from '@material-ui/core';
|
||||
import {
|
||||
Icon,
|
||||
Table,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
IconButton,
|
||||
} from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { formatFullDateTimeWithLocale } from '../../../component/common/util';
|
||||
import CreateApiKey from './api-key-create';
|
||||
@ -9,9 +17,19 @@ import Secret from './secret';
|
||||
import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender';
|
||||
import Dialogue from '../../../component/common/Dialogue/Dialogue';
|
||||
import AccessContext from '../../../contexts/AccessContext';
|
||||
import { DELETE_API_TOKEN, CREATE_API_TOKEN } from '../../../component/AccessProvider/permissions';
|
||||
import {
|
||||
DELETE_API_TOKEN,
|
||||
CREATE_API_TOKEN,
|
||||
} from '../../../component/AccessProvider/permissions';
|
||||
|
||||
function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, unleashUrl }) {
|
||||
function ApiKeyList({
|
||||
location,
|
||||
fetchApiKeys,
|
||||
removeKey,
|
||||
addKey,
|
||||
keys,
|
||||
unleashUrl,
|
||||
}) {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [delKey, setDelKey] = useState(undefined);
|
||||
@ -28,21 +46,28 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, unleashUr
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Alert severity="info" >
|
||||
<Alert severity="info">
|
||||
<p>
|
||||
Read the{' '}
|
||||
<a href="https://docs.getunleash.io/docs" target="_blank" rel="noreferrer">
|
||||
<a
|
||||
href="https://docs.getunleash.io/docs"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Getting started guide
|
||||
</a>{' '}
|
||||
to learn how to connect to the Unleash API form your application or programmatically.
|
||||
Please note it can take up to 1 minute before a new API key is activated.
|
||||
to learn how to connect to the Unleash API from your
|
||||
application or programmatically. Please note it can take up
|
||||
to 1 minute before a new API key is activated.
|
||||
</p>
|
||||
<br />
|
||||
<strong>API URL: </strong> <pre style={{display: 'inline'}}>{unleashUrl}/api/</pre>
|
||||
<strong>API URL: </strong>{' '}
|
||||
<pre style={{ display: 'inline' }}>{unleashUrl}/api/</pre>
|
||||
</Alert>
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<br />
|
||||
<Table>
|
||||
<TableHead>
|
||||
@ -58,10 +83,17 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, unleashUr
|
||||
{keys.map(item => (
|
||||
<TableRow key={item.secret}>
|
||||
<TableCell style={{ textAlign: 'left' }}>
|
||||
{formatFullDateTimeWithLocale(item.createdAt, location.locale)}
|
||||
{formatFullDateTimeWithLocale(
|
||||
item.createdAt,
|
||||
location.locale
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: 'left' }}>
|
||||
{item.username}
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: 'left' }}>
|
||||
{item.type}
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: 'left' }}>{item.username}</TableCell>
|
||||
<TableCell style={{ textAlign: 'left' }}>{item.type}</TableCell>
|
||||
<TableCell style={{ textAlign: 'left' }}>
|
||||
<Secret value={item.secret} />
|
||||
</TableCell>
|
||||
@ -95,7 +127,10 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, unleashUr
|
||||
>
|
||||
<div>Are you sure you want to delete?</div>
|
||||
</Dialogue>
|
||||
<ConditionallyRender condition={hasAccess(CREATE_API_TOKEN)} show={<CreateApiKey addKey={addKey} />} />
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(CREATE_API_TOKEN)}
|
||||
show={<CreateApiKey addKey={addKey} />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -12,7 +12,12 @@ const initialState = {
|
||||
unleashHostname: location.hostname,
|
||||
};
|
||||
|
||||
function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl }) {
|
||||
function GoogleAuth({
|
||||
config,
|
||||
getGoogleConfig,
|
||||
updateGoogleConfig,
|
||||
unleashUrl,
|
||||
}) {
|
||||
const [data, setData] = useState(initialState);
|
||||
const [info, setInfo] = useState();
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
@ -64,11 +69,16 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
|
||||
<Grid item xs={12}>
|
||||
<Alert severity="info">
|
||||
Please read the{' '}
|
||||
<a href="https://www.unleash-hosted.com/docs/enterprise-authentication/google" target="_blank" rel="noreferrer">
|
||||
<a
|
||||
href="https://www.unleash-hosted.com/docs/enterprise-authentication/google"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
documentation
|
||||
</a>{' '}
|
||||
to learn how to integrate with Google OAuth 2.0. <br />
|
||||
Callback URL: <code>{unleashUrl}/auth/google/callback</code>
|
||||
Callback URL:{' '}
|
||||
<code>{unleashUrl}/auth/google/callback</code>
|
||||
</Alert>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@ -77,12 +87,16 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
|
||||
<Grid item xs={5}>
|
||||
<strong>Enable</strong>
|
||||
<p>
|
||||
Enable Google users to login. Value is ignored if Client ID and Client Secret are not
|
||||
defined.
|
||||
Enable Google users to login. Value is ignored if
|
||||
Client ID and Client Secret are not defined.
|
||||
</p>
|
||||
</Grid>
|
||||
<Grid item xs={6} style={{ padding: '20px' }}>
|
||||
<Switch onChange={updateEnabled} name="enabled" checked={data.enabled}>
|
||||
<Switch
|
||||
onChange={updateEnabled}
|
||||
name="enabled"
|
||||
checked={data.enabled}
|
||||
>
|
||||
{data.enabled ? 'Enabled' : 'Disabled'}
|
||||
</Switch>
|
||||
</Grid>
|
||||
@ -90,7 +104,10 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={5}>
|
||||
<strong>Client ID</strong>
|
||||
<p>(Required) The Client ID provided by Google when registering the application.</p>
|
||||
<p>
|
||||
(Required) The Client ID provided by Google when
|
||||
registering the application.
|
||||
</p>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<TextField
|
||||
@ -108,7 +125,10 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={5}>
|
||||
<strong>Client Secret</strong>
|
||||
<p>(Required) Client Secret provided by Google when registering the application.</p>
|
||||
<p>
|
||||
(Required) Client Secret provided by Google when
|
||||
registering the application.
|
||||
</p>
|
||||
</Grid>
|
||||
<Grid item md={6}>
|
||||
<TextField
|
||||
@ -127,10 +147,13 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
|
||||
<Grid item md={5}>
|
||||
<strong>Unleash hostname</strong>
|
||||
<p>
|
||||
(Required) The hostname you are running Unleash on that Google should send the user back to.
|
||||
The final callback URL will be{' '}
|
||||
(Required) The hostname you are running Unleash on
|
||||
that Google should send the user back to. The final
|
||||
callback URL will be{' '}
|
||||
<small>
|
||||
<code>https://[unleash.hostname.com]/auth/google/callback</code>
|
||||
<code>
|
||||
https://[unleash.hostname.com]/auth/google/callback
|
||||
</code>
|
||||
</small>
|
||||
</p>
|
||||
</Grid>
|
||||
@ -150,10 +173,17 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={5}>
|
||||
<strong>Auto-create users</strong>
|
||||
<p>Enable automatic creation of new users when signing in with Google.</p>
|
||||
<p>
|
||||
Enable automatic creation of new users when signing
|
||||
in with Google.
|
||||
</p>
|
||||
</Grid>
|
||||
<Grid item md={6} style={{ padding: '20px' }}>
|
||||
<Switch onChange={updateAutoCreate} name="enabled" checked={data.autoCreate}>
|
||||
<Switch
|
||||
onChange={updateAutoCreate}
|
||||
name="enabled"
|
||||
checked={data.autoCreate}
|
||||
>
|
||||
Auto-create users
|
||||
</Switch>
|
||||
</Grid>
|
||||
@ -161,7 +191,10 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={5}>
|
||||
<strong>Email domains</strong>
|
||||
<p>(Optional) Comma separated list of email domains that should be allowed to sign in.</p>
|
||||
<p>
|
||||
(Optional) Comma separated list of email domains
|
||||
that should be allowed to sign in.
|
||||
</p>
|
||||
</Grid>
|
||||
<Grid item md={6}>
|
||||
<TextField
|
||||
@ -180,7 +213,11 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
|
||||
</Grid>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={5}>
|
||||
<Button variant="contained" color="primary" type="submit">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>{' '}
|
||||
<small>{info}</small>
|
||||
|
@ -4,7 +4,7 @@ import { Button, Grid, Switch, TextField } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import PageContent from '../../../component/common/PageContent/PageContent';
|
||||
import AccessContext from '../../../contexts/AccessContext';
|
||||
import { ADMIN } from '../../../component/AccessProvider/permissions';
|
||||
import { ADMIN } from '../../../component/AccessProvider/permissions';
|
||||
|
||||
const initialState = {
|
||||
enabled: false,
|
||||
@ -30,7 +30,11 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
||||
}, [config]);
|
||||
|
||||
if (!hasAccess(ADMIN)) {
|
||||
return <Alert severity="error">You need to be a root admin to access this section.</Alert>;
|
||||
return (
|
||||
<Alert severity="error">
|
||||
You need to be a root admin to access this section.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const updateField = e => {
|
||||
@ -65,11 +69,17 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
||||
<Grid item md={12}>
|
||||
<Alert severity="info">
|
||||
Please read the{' '}
|
||||
<a href="https://www.unleash-hosted.com/docs/enterprise-authentication" target="_blank" rel="noreferrer">
|
||||
<a
|
||||
href="https://www.unleash-hosted.com/docs/enterprise-authentication"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
documentation
|
||||
</a>{' '}
|
||||
to learn how to integrate with specific SAML 2.0 providers (Okta, Keycloak, etc). <br />
|
||||
Callback URL: <code>{unleashUrl}/auth/saml/callback</code>
|
||||
to learn how to integrate with specific SAML 2.0
|
||||
providers (Okta, Keycloak, etc). <br />
|
||||
Callback URL:{' '}
|
||||
<code>{unleashUrl}/auth/saml/callback</code>
|
||||
</Alert>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@ -80,7 +90,12 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
||||
<p>Enable SAML 2.0 Authentication.</p>
|
||||
</Grid>
|
||||
<Grid item md={6}>
|
||||
<Switch onChange={updateEnabled} value={data.enabled} name="enabled" checked={data.enabled}>
|
||||
<Switch
|
||||
onChange={updateEnabled}
|
||||
value={data.enabled}
|
||||
name="enabled"
|
||||
checked={data.enabled}
|
||||
>
|
||||
{data.enabled ? 'Enabled' : 'Disabled'}
|
||||
</Switch>
|
||||
</Grid>
|
||||
@ -105,7 +120,10 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={5}>
|
||||
<strong>Single Sign-On URL</strong>
|
||||
<p>(Required) The url to redirect the user to for signing in.</p>
|
||||
<p>
|
||||
(Required) The url to redirect the user to for
|
||||
signing in.
|
||||
</p>
|
||||
</Grid>
|
||||
<Grid item md={6}>
|
||||
<TextField
|
||||
@ -122,7 +140,10 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={5}>
|
||||
<strong>X.509 Certificate</strong>
|
||||
<p>(Required) The certificate used to sign the SAML 2.0 request.</p>
|
||||
<p>
|
||||
(Required) The certificate used to sign the SAML 2.0
|
||||
request.
|
||||
</p>
|
||||
</Grid>
|
||||
<Grid item md={7}>
|
||||
<TextField
|
||||
@ -146,10 +167,17 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={5}>
|
||||
<strong>Auto-create users</strong>
|
||||
<p>Enable automatic creation of new users when signing in with Saml.</p>
|
||||
<p>
|
||||
Enable automatic creation of new users when signing
|
||||
in with Saml.
|
||||
</p>
|
||||
</Grid>
|
||||
<Grid item md={6} style={{ padding: '20px' }}>
|
||||
<Switch onChange={updateAutoCreate} name="enabled" checked={data.autoCreate}>
|
||||
<Switch
|
||||
onChange={updateAutoCreate}
|
||||
name="enabled"
|
||||
checked={data.autoCreate}
|
||||
>
|
||||
Auto-create users
|
||||
</Switch>
|
||||
</Grid>
|
||||
@ -157,7 +185,10 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={5}>
|
||||
<strong>Email domains</strong>
|
||||
<p>(Optional) Comma separated list of email domains that should be allowed to sign in.</p>
|
||||
<p>
|
||||
(Optional) Comma separated list of email domains
|
||||
that should be allowed to sign in.
|
||||
</p>
|
||||
</Grid>
|
||||
<Grid item md={6}>
|
||||
<TextField
|
||||
@ -175,7 +206,11 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
||||
</Grid>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={5}>
|
||||
<Button variant="contained" color="primary" type="submit">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>{' '}
|
||||
<small>{info}</small>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Typography } from '@material-ui/core';
|
||||
import Dialogue from '../../../../../component/common/Dialogue';
|
||||
|
||||
import { ReactComponent as EmailIcon } from '../../../../../icons/email.svg';
|
||||
import { ReactComponent as EmailIcon } from '../../../../../assets/icons/email.svg';
|
||||
import { useStyles } from './ConfirmUserEmail.styles';
|
||||
|
||||
interface IConfirmUserEmailProps {
|
||||
|
@ -251,4 +251,4 @@ UsersList.propTypes = {
|
||||
location: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default UsersList;
|
||||
export default UsersList;
|
||||
|
@ -8,22 +8,26 @@ import ConditionallyRender from '../../../component/common/ConditionallyRender';
|
||||
import { ADMIN } from '../../../component/AccessProvider/permissions';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
|
||||
const UsersAdmin = ({history}) => {
|
||||
const UsersAdmin = ({ history }) => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminMenu history={history} />
|
||||
<PageContent headerContent="Users">
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(ADMIN)}
|
||||
show={<UsersList />}
|
||||
elseShow={<Alert severity="error">You need to be a root admin to access this section.</Alert>} />
|
||||
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(ADMIN)}
|
||||
show={<UsersList />}
|
||||
elseShow={
|
||||
<Alert severity="error">
|
||||
You need to be a root admin to access this section.
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
</PageContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
UsersAdmin.propTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { formatApiPath } from '../../utils/format-path';
|
||||
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||
|
||||
const URI = 'api/admin/addons';
|
||||
const URI = formatApiPath(`api/admin/addons`);
|
||||
|
||||
function fetchAll() {
|
||||
return fetch(URI, { credentials: 'include' })
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { formatApiPath } from '../../utils/format-path';
|
||||
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||
|
||||
const URI = 'api/admin/metrics/applications';
|
||||
const URI = formatApiPath('api/admin/metrics/applications');
|
||||
|
||||
function fetchAll() {
|
||||
return fetch(URI, { headers, credentials: 'include' })
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { formatApiPath } from '../../utils/format-path';
|
||||
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||
|
||||
const URI = 'api/admin/archive';
|
||||
const URI = formatApiPath('api/admin/archive');
|
||||
|
||||
function fetchAll() {
|
||||
return fetch(`${URI}/features`, { credentials: 'include' })
|
||||
|
@ -10,7 +10,7 @@ export const ERROR_ADD_CONTEXT_FIELD = 'ERROR_ADD_CONTEXT_FIELD';
|
||||
export const UPDATE_CONTEXT_FIELD = 'UPDATE_CONTEXT_FIELD';
|
||||
export const ERROR_UPDATE_CONTEXT_FIELD = 'ERROR_UPDATE_CONTEXT_FIELD';
|
||||
|
||||
const receiveContext = value => ({ type: RECEIVE_CONTEXT, value });
|
||||
export const receiveContext = value => ({ type: RECEIVE_CONTEXT, value });
|
||||
const addContextField = context => ({ type: ADD_CONTEXT_FIELD, context });
|
||||
const upContextField = context => ({ type: UPDATE_CONTEXT_FIELD, context });
|
||||
const createRemoveContext = context => ({ type: REMOVE_CONTEXT, context });
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { formatApiPath } from '../../utils/format-path';
|
||||
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||
|
||||
const URI = 'api/admin/context';
|
||||
const URI = formatApiPath('api/admin/context');
|
||||
|
||||
function fetchAll() {
|
||||
return fetch(URI, { credentials: 'include' })
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { List } from 'immutable';
|
||||
import { RECEIVE_CONTEXT, REMOVE_CONTEXT, ADD_CONTEXT_FIELD, UPDATE_CONTEXT_FIELD } from './actions';
|
||||
import {
|
||||
RECEIVE_CONTEXT,
|
||||
REMOVE_CONTEXT,
|
||||
ADD_CONTEXT_FIELD,
|
||||
UPDATE_CONTEXT_FIELD,
|
||||
} from './actions';
|
||||
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
|
||||
|
||||
const DEFAULT_CONTEXT_FIELDS = [
|
||||
@ -21,7 +26,9 @@ const strategies = (state = getInitState(), action) => {
|
||||
case ADD_CONTEXT_FIELD:
|
||||
return state.push(action.context);
|
||||
case UPDATE_CONTEXT_FIELD: {
|
||||
const index = state.findIndex(item => item.name === action.context.name);
|
||||
const index = state.findIndex(
|
||||
item => item.name === action.context.name
|
||||
);
|
||||
return state.set(index, action.context);
|
||||
}
|
||||
case USER_LOGOUT:
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { formatApiPath } from '../../utils/format-path';
|
||||
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||
|
||||
const URI = 'api/admin/api-tokens';
|
||||
const URI = formatApiPath('api/admin/api-tokens');
|
||||
|
||||
function fetchAll() {
|
||||
return fetch(URI, { headers, credentials: 'include' })
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { formatApiPath } from '../../utils/format-path';
|
||||
import { throwIfNotSuccess } from '../api-helper';
|
||||
|
||||
const URI = 'api/admin/metrics/feature-toggles';
|
||||
const URI = formatApiPath('api/admin/metrics/feature-toggles');
|
||||
|
||||
function fetchFeatureMetrics() {
|
||||
return fetch(URI, { credentials: 'include' })
|
||||
@ -8,7 +9,7 @@ function fetchFeatureMetrics() {
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
const seenURI = 'api/admin/metrics/seen-apps';
|
||||
const seenURI = formatApiPath('api/admin/metrics/seen-apps');
|
||||
|
||||
function fetchSeenApps() {
|
||||
return fetch(seenURI, { credentials: 'include' })
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { formatApiPath } from '../../utils/format-path';
|
||||
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||
|
||||
const URI = 'api/admin/features';
|
||||
const URI = formatApiPath('api/admin/features');
|
||||
|
||||
function tagFeature(featureToggle, tag) {
|
||||
return fetch(`${URI}/${featureToggle}/tags`, {
|
||||
@ -14,11 +15,16 @@ function tagFeature(featureToggle, tag) {
|
||||
}
|
||||
|
||||
function untagFeature(featureToggle, tag) {
|
||||
return fetch(`${URI}/${featureToggle}/tags/${tag.type}/${encodeURIComponent(tag.value)}`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
credentials: 'include',
|
||||
}).then(throwIfNotSuccess);
|
||||
return fetch(
|
||||
`${URI}/${featureToggle}/tags/${tag.type}/${encodeURIComponent(
|
||||
tag.value
|
||||
)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
credentials: 'include',
|
||||
}
|
||||
).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
function fetchFeatureToggleTags(featureToggle) {
|
||||
|
@ -1,10 +1,14 @@
|
||||
import { formatApiPath } from '../../utils/format-path';
|
||||
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||
|
||||
const URI = 'api/admin/features';
|
||||
const URI = formatApiPath('api/admin/features');
|
||||
|
||||
function validateToggle(featureToggle) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!featureToggle.strategies || featureToggle.strategies.length === 0) {
|
||||
if (
|
||||
!featureToggle.strategies ||
|
||||
featureToggle.strategies.length === 0
|
||||
) {
|
||||
reject(new Error('You must add at least one activation strategy'));
|
||||
} else {
|
||||
resolve(featureToggle);
|
||||
|
@ -4,12 +4,15 @@ import { dispatchError } from '../util';
|
||||
export const RECEIVE_FEATURE_TYPES = 'RECEIVE_FEATURE_TYPES';
|
||||
export const ERROR_RECEIVE_FEATURE_TYPES = 'ERROR_RECEIVE_FEATURE_TYPES';
|
||||
|
||||
const receiveFeatureTypes = value => ({ type: RECEIVE_FEATURE_TYPES, value });
|
||||
export const receiveFeatureTypes = value => ({
|
||||
type: RECEIVE_FEATURE_TYPES,
|
||||
value,
|
||||
});
|
||||
|
||||
export function fetchFeatureTypes() {
|
||||
return dispatch =>
|
||||
api
|
||||
.fetchAll()
|
||||
.then(json => dispatch(receiveFeatureTypes(json)))
|
||||
.then(json => dispatch(receiveFeatureTypes(json.types)))
|
||||
.catch(dispatchError(dispatch, ERROR_RECEIVE_FEATURE_TYPES));
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { formatApiPath } from '../../utils/format-path';
|
||||
import { throwIfNotSuccess } from '../api-helper';
|
||||
|
||||
const URI = 'api/admin/feature-types';
|
||||
const URI = formatApiPath('api/admin/feature-types');
|
||||
|
||||
function fetchAll() {
|
||||
return fetch(URI, { credentials: 'include' })
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { List } from 'immutable';
|
||||
import { RECEIVE_FEATURE_TYPES } from './actions';
|
||||
|
||||
const DEFAULT_FEATURE_TYPES = [{ id: 'release', name: 'Release', initial: true }];
|
||||
const DEFAULT_FEATURE_TYPES = [
|
||||
{ id: 'release', name: 'Release', initial: true },
|
||||
];
|
||||
|
||||
function getInitState() {
|
||||
return new List(DEFAULT_FEATURE_TYPES);
|
||||
@ -10,7 +12,7 @@ function getInitState() {
|
||||
const strategies = (state = getInitState(), action) => {
|
||||
switch (action.type) {
|
||||
case RECEIVE_FEATURE_TYPES:
|
||||
return new List(action.value.types);
|
||||
return new List(action.value);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { formatApiPath } from '../../utils/format-path';
|
||||
import { throwIfNotSuccess } from '../api-helper';
|
||||
|
||||
const URI = 'api/admin/events';
|
||||
const URI = formatApiPath('api/admin/events');
|
||||
|
||||
function fetchAll() {
|
||||
return fetch(URI, { credentials: 'include' })
|
||||
|
@ -1,18 +0,0 @@
|
||||
import { fetchUIConfig } from './ui-config/actions';
|
||||
import { fetchContext } from './context/actions';
|
||||
import { fetchFeatureTypes } from './feature-type/actions';
|
||||
import { fetchProjects } from './project/actions';
|
||||
import { fetchStrategies } from './strategy/actions';
|
||||
import { fetchTagTypes } from './tag-type/actions';
|
||||
import { C, P } from '../component/common/flags';
|
||||
|
||||
export function loadInitialData(flags = {}) {
|
||||
return dispatch => {
|
||||
fetchUIConfig()(dispatch);
|
||||
if (flags[C]) fetchContext()(dispatch);
|
||||
fetchFeatureTypes()(dispatch);
|
||||
if (flags[P]) fetchProjects()(dispatch);
|
||||
fetchStrategies()(dispatch);
|
||||
fetchTagTypes()(dispatch);
|
||||
};
|
||||
}
|
@ -13,9 +13,9 @@ export const ERROR_UPDATE_PROJECT = 'ERROR_UPDATE_PROJECT';
|
||||
const addProject = project => ({ type: ADD_PROJECT, project });
|
||||
const upProject = project => ({ type: UPDATE_PROJECT, project });
|
||||
const delProject = project => ({ type: REMOVE_PROJECT, project });
|
||||
export const receiveProjects = value => ({ type: RECEIVE_PROJECT, value });
|
||||
|
||||
export function fetchProjects() {
|
||||
const receiveProjects = value => ({ type: RECEIVE_PROJECT, value });
|
||||
return dispatch =>
|
||||
api
|
||||
.fetchAll()
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { formatApiPath } from '../../utils/format-path';
|
||||
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||
|
||||
const URI = 'api/admin/projects';
|
||||
const URI = formatApiPath('api/admin/projects');
|
||||
|
||||
function fetchAll() {
|
||||
return fetch(URI, { credentials: 'include' })
|
||||
|
@ -20,22 +20,31 @@ export const ERROR_REMOVING_STRATEGY = 'ERROR_REMOVING_STRATEGY';
|
||||
export const ERROR_DEPRECATING_STRATEGY = 'ERROR_DEPRECATING_STRATEGY';
|
||||
export const ERROR_REACTIVATING_STRATEGY = 'ERROR_REACTIVATING_STRATEGY';
|
||||
|
||||
export const receiveStrategies = json => ({
|
||||
type: RECEIVE_STRATEGIES,
|
||||
value: json,
|
||||
});
|
||||
|
||||
const addStrategy = strategy => ({ type: ADD_STRATEGY, strategy });
|
||||
const createRemoveStrategy = strategy => ({ type: REMOVE_STRATEGY, strategy });
|
||||
const updatedStrategy = strategy => ({ type: UPDATE_STRATEGY, strategy });
|
||||
|
||||
const startRequest = () => ({ type: REQUEST_STRATEGIES });
|
||||
|
||||
const receiveStrategies = json => ({ type: RECEIVE_STRATEGIES, value: json.strategies });
|
||||
|
||||
const startCreate = () => ({ type: START_CREATE_STRATEGY });
|
||||
|
||||
const startUpdate = () => ({ type: START_UPDATE_STRATEGY });
|
||||
|
||||
const startDeprecate = () => ({ type: START_DEPRECATE_STRATEGY });
|
||||
const deprecateStrategyEvent = strategy => ({ type: DEPRECATE_STRATEGY, strategy });
|
||||
const deprecateStrategyEvent = strategy => ({
|
||||
type: DEPRECATE_STRATEGY,
|
||||
strategy,
|
||||
});
|
||||
const startReactivate = () => ({ type: START_REACTIVATE_STRATEGY });
|
||||
const reactivateStrategyEvent = strategy => ({ type: REACTIVATE_STRATEGY, strategy });
|
||||
const reactivateStrategyEvent = strategy => ({
|
||||
type: REACTIVATE_STRATEGY,
|
||||
strategy,
|
||||
});
|
||||
|
||||
export function fetchStrategies() {
|
||||
return dispatch => {
|
||||
@ -43,7 +52,7 @@ export function fetchStrategies() {
|
||||
|
||||
return api
|
||||
.fetchAll()
|
||||
.then(json => dispatch(receiveStrategies(json)))
|
||||
.then(json => dispatch(receiveStrategies(json.strategies)))
|
||||
.catch(dispatchError(dispatch, ERROR_RECEIVE_STRATEGIES));
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { formatApiPath } from '../../utils/format-path';
|
||||
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||
|
||||
const URI = 'api/admin/strategies';
|
||||
const URI = formatApiPath('api/admin/strategies');
|
||||
|
||||
function fetchAll() {
|
||||
return fetch(URI, { credentials: 'include' })
|
||||
|
@ -14,7 +14,7 @@ export const START_UPDATE_TAG_TYPE = 'START_UPDATE_TAG_TYPE';
|
||||
export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE';
|
||||
export const ERROR_UPDATE_TAG_TYPE = 'ERROR_UPDATE_TAG_TYPE';
|
||||
|
||||
function receiveTagTypes(json) {
|
||||
export function receiveTagTypes(json) {
|
||||
return {
|
||||
type: RECEIVE_TAG_TYPES,
|
||||
value: json,
|
||||
@ -37,7 +37,12 @@ export function createTagType({ name, description, icon }) {
|
||||
dispatch({ type: START_CREATE_TAG_TYPE });
|
||||
return api
|
||||
.create({ name, description, icon })
|
||||
.then(() => dispatch({ type: ADD_TAG_TYPE, tagType: { name, description, icon } }))
|
||||
.then(() =>
|
||||
dispatch({
|
||||
type: ADD_TAG_TYPE,
|
||||
tagType: { name, description, icon },
|
||||
})
|
||||
)
|
||||
.catch(dispatchError(dispatch, ERROR_CREATE_TAG_TYPE));
|
||||
};
|
||||
}
|
||||
@ -47,7 +52,12 @@ export function updateTagType({ name, description, icon }) {
|
||||
dispatch({ type: START_UPDATE_TAG_TYPE });
|
||||
return api
|
||||
.update({ name, description, icon })
|
||||
.then(() => dispatch({ type: UPDATE_TAG_TYPE, tagType: { name, description, icon } }))
|
||||
.then(() =>
|
||||
dispatch({
|
||||
type: UPDATE_TAG_TYPE,
|
||||
tagType: { name, description, icon },
|
||||
})
|
||||
)
|
||||
.catch(dispatchError(dispatch, ERROR_UPDATE_TAG_TYPE));
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { formatApiPath } from '../../utils/format-path';
|
||||
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||
|
||||
const URI = 'api/admin/tag-types';
|
||||
const URI = formatApiPath('api/admin/tag-types');
|
||||
|
||||
function fetchTagTypes() {
|
||||
return fetch(URI, { credentials: 'include' })
|
||||
@ -53,5 +54,5 @@ const api = {
|
||||
update,
|
||||
deleteTagType,
|
||||
validateTagType,
|
||||
}
|
||||
};
|
||||
export default api;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { formatApiPath } from '../../utils/format-path';
|
||||
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||
|
||||
const URI = 'api/admin/tags';
|
||||
const URI = formatApiPath('api/admin/tags');
|
||||
|
||||
function fetchTags() {
|
||||
return fetch(URI, { credentials: 'include' })
|
||||
|
26
frontend/src/store/ui-bootstrap/actions.js
Normal file
@ -0,0 +1,26 @@
|
||||
import api from './api';
|
||||
import { dispatchError } from '../util';
|
||||
import { receiveConfig } from '../ui-config/actions';
|
||||
import { receiveContext } from '../context/actions';
|
||||
import { receiveFeatureTypes } from '../feature-type/actions';
|
||||
import { receiveProjects } from '../project/actions';
|
||||
import { receiveTagTypes } from '../tag-type/actions';
|
||||
import { receiveStrategies } from '../strategy/actions';
|
||||
|
||||
export const RECEIVE_BOOTSTRAP = 'RECEIVE_CONFIG';
|
||||
export const ERROR_RECEIVE_BOOTSTRAP = 'ERROR_RECEIVE_CONFIG';
|
||||
|
||||
export function fetchUiBootstrap() {
|
||||
return dispatch =>
|
||||
api
|
||||
.fetchUIBootstrap()
|
||||
.then(json => {
|
||||
dispatch(receiveProjects(json.projects));
|
||||
dispatch(receiveConfig(json.uiConfig));
|
||||
dispatch(receiveContext(json.context));
|
||||
dispatch(receiveTagTypes(json));
|
||||
dispatch(receiveFeatureTypes(json.featureTypes));
|
||||
dispatch(receiveStrategies(json.strategies));
|
||||
})
|
||||
.catch(dispatchError(dispatch, ERROR_RECEIVE_BOOTSTRAP));
|
||||
}
|
14
frontend/src/store/ui-bootstrap/api.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { formatApiPath } from '../../utils/format-path';
|
||||
import { throwIfNotSuccess } from '../api-helper';
|
||||
|
||||
const URI = formatApiPath('api/admin/ui-bootstrap');
|
||||
|
||||
function fetchUIBootstrap() {
|
||||
return fetch(URI, { credentials: 'include' })
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchUIBootstrap,
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import { formatApiPath } from '../../utils/format-path';
|
||||
import { throwIfNotSuccess } from '../api-helper';
|
||||
|
||||
const URI = 'api/admin/ui-config';
|
||||
const URI = formatApiPath('api/admin/ui-config');
|
||||
|
||||
function fetchConfig() {
|
||||
return fetch(URI, { credentials: 'include' })
|
||||
|
@ -1,6 +1,7 @@
|
||||
import api from './api';
|
||||
import { dispatchError } from '../util';
|
||||
import { RESET_LOADING } from '../feature-toggle/actions';
|
||||
import { getBasePath } from '../../utils/format-path';
|
||||
export const USER_CHANGE_CURRENT = 'USER_CHANGE_CURRENT';
|
||||
export const USER_LOGOUT = 'USER_LOGOUT';
|
||||
export const USER_LOGIN = 'USER_LOGIN';
|
||||
@ -63,12 +64,15 @@ export function passwordLogin(path, user) {
|
||||
}
|
||||
|
||||
export function logoutUser() {
|
||||
const basepath = getBasePath();
|
||||
return dispatch => {
|
||||
return api
|
||||
.logoutUser()
|
||||
.then(() => dispatch({ type: USER_LOGOUT }))
|
||||
.then(() => dispatch({ type: RESET_LOADING }))
|
||||
.then(() => (window.location = '/'))
|
||||
.then(() => {
|
||||
window.location = `${basepath}`;
|
||||
})
|
||||
.catch(handleError);
|
||||
};
|
||||
}
|
||||
|