1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-23 13:46:45 +02:00

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
This commit is contained in:
Fredrik Strand Oseberg 2021-05-04 09:59:42 +02:00 committed by GitHub
parent c2e50bf16c
commit f0d6e45361
102 changed files with 1390 additions and 569 deletions

View File

@ -1,17 +1,24 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="baseUriPath" content="::baseUriPath::" />
<meta name="description" content="unleash"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="unleash" />
<title>Unleash - Enterprise ready feature toggles</title> <title>Unleash - Enterprise ready feature toggles</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet"> href="https://fonts.googleapis.com/icon?family=Material+Icons"
</head> rel="stylesheet"
<body> />
<div id='app'></div> <link
</body> href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700"
rel="stylesheet"
/>
</head>
<body>
<div id="app"></div>
</body>
</html> </html>

View 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

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View 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

View 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

View File

Before

Width:  |  Height:  |  Size: 419 B

After

Width:  |  Height:  |  Size: 419 B

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View 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

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 229 B

After

Width:  |  Height:  |  Size: 229 B

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,7 +1,7 @@
import { FC } from "react"; import { FC } from 'react';
import AccessContext from '../../contexts/AccessContext' import AccessContext from '../../contexts/AccessContext';
import { ADMIN } from "./permissions"; import { ADMIN } from './permissions';
// TODO: Type up redux store // TODO: Type up redux store
interface IAccessProvider { interface IAccessProvider {
@ -9,30 +9,34 @@ interface IAccessProvider {
} }
interface IPermission { interface IPermission {
permission: string; permission: string;
project: string | null; project: string | null;
} }
const AccessProvider: FC<IAccessProvider> = ({store, children}) => { const AccessProvider: FC<IAccessProvider> = ({ store, children }) => {
const hasAccess = (permission: string, project: string) => { const hasAccess = (permission: string, project: string) => {
const permissions = store.getState().user.get('permissions') || []; const permissions = store.getState().user.get('permissions') || [];
const result = permissions.some((p: IPermission) => { const result = permissions.some((p: IPermission) => {
if(p.permission === ADMIN) { if (p.permission === ADMIN) {
return true return true;
} }
if(p.permission === permission && p.project === project) { if (p.permission === permission && p.project === project) {
return true; return true;
} }
return false; 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;

View File

@ -10,11 +10,18 @@ import { routes } from './menu/routes';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import IAuthStatus from '../interfaces/user'; import IAuthStatus from '../interfaces/user';
import { useEffect } from 'react';
interface IAppProps extends RouteComponentProps { interface IAppProps extends RouteComponentProps {
user: IAuthStatus; user: IAuthStatus;
fetchUiBootstrap: any;
} }
const App = ({ location, user }: IAppProps) => { const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
useEffect(() => {
fetchUiBootstrap();
/* eslint-disable-next-line */
}, []);
const renderMainLayoutRoutes = () => { const renderMainLayoutRoutes = () => {
return routes.filter(route => route.layout === 'main').map(renderRoute); return routes.filter(route => route.layout === 'main').map(renderRoute);
}; };

View 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);

View File

@ -6,6 +6,13 @@ import { Avatar, Icon } from '@material-ui/core';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import AccessContext from '../../../contexts/AccessContext'; 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 = { const style = {
width: '40px', width: '40px',
height: '40px', height: '40px',
@ -16,15 +23,45 @@ const style = {
const getIcon = name => { const getIcon = name => {
switch (name) { switch (name) {
case 'slack': 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': 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': 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': 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': case 'datadog':
return <img style={style} alt="Datadog" src="datadog.svg" />; return (
<img
style={style}
alt="Datadog"
src={formatAssetPath(dataDogIcon)}
/>
);
default: default:
return ( return (
<Avatar> <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); const { hasAccess } = useContext(AccessContext);
useEffect(() => { useEffect(() => {
if (addons.length === 0) { if (addons.length === 0) {
@ -59,7 +103,12 @@ const AddonList = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, h
/> />
<br /> <br />
<AvailableAddons providers={providers} hasAccess={hasAccess} history={history} getIcon={getIcon} /> <AvailableAddons
providers={providers}
hasAccess={hasAccess}
history={history}
getIcon={getIcon}
/>
</> </>
); );
}; };

View File

@ -1,6 +1,13 @@
import React from 'react'; import React from 'react';
import PageContent from '../../../common/PageContent/PageContent'; 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 ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
import { CREATE_ADDON } from '../../../AccessProvider/permissions'; import { CREATE_ADDON } from '../../../AccessProvider/permissions';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -9,7 +16,10 @@ const AvailableAddons = ({ providers, getIcon, hasAccess, history }) => {
const renderProvider = provider => ( const renderProvider = provider => (
<ListItem key={provider.name}> <ListItem key={provider.name}>
<ListItemAvatar>{getIcon(provider.name)}</ListItemAvatar> <ListItemAvatar>{getIcon(provider.name)}</ListItemAvatar>
<ListItemText primary={provider.displayName} secondary={provider.description} /> <ListItemText
primary={provider.displayName}
secondary={provider.description}
/>
<ListItemSecondaryAction> <ListItemSecondaryAction>
<ConditionallyRender <ConditionallyRender
condition={hasAccess(CREATE_ADDON)} condition={hasAccess(CREATE_ADDON)}
@ -17,7 +27,9 @@ const AvailableAddons = ({ providers, getIcon, hasAccess, history }) => {
<Button <Button
variant="contained" variant="contained"
name="device_hub" name="device_hub"
onClick={() => history.push(`/addons/create/${provider.name}`)} onClick={() =>
history.push(`/addons/create/${provider.name}`)
}
title="Configure" title="Configure"
> >
Configure Configure

View File

@ -495,6 +495,7 @@ exports[`renders correctly with permissions 1`] = `
<span> <span>
123.123.123.123 123.123.123.123
last seen at last seen at
<small> <small>
02/23/2017, 03:56:49 PM 02/23/2017, 03:56:49 PM
</small> </small>

View File

@ -4,7 +4,12 @@ import { ThemeProvider } from '@material-ui/core';
import ClientApplications from '../application-edit-component'; import ClientApplications from '../application-edit-component';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom'; 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 theme from '../../../themes/main-theme';
import { createFakeStore } from '../../../accessStoreFake'; import { createFakeStore } from '../../../accessStoreFake';
@ -13,7 +18,7 @@ import AccessProvider from '../../AccessProvider/AccessProvider';
test('renders correctly if no application', () => { test('renders correctly if no application', () => {
const tree = renderer const tree = renderer
.create( .create(
<AccessProvider store={createFakeStore([{permission: ADMIN}])}> <AccessProvider store={createFakeStore([{ permission: ADMIN }])}>
<ClientApplications <ClientApplications
fetchApplication={() => Promise.resolve({})} fetchApplication={() => Promise.resolve({})}
storeApplicationMetaData={jest.fn()} storeApplicationMetaData={jest.fn()}
@ -32,51 +37,51 @@ test('renders correctly without permission', () => {
.create( .create(
<MemoryRouter> <MemoryRouter>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<AccessProvider store={createFakeStore([])}> <AccessProvider store={createFakeStore([])}>
<ClientApplications <ClientApplications
fetchApplication={() => Promise.resolve({})} fetchApplication={() => Promise.resolve({})}
storeApplicationMetaData={jest.fn()} storeApplicationMetaData={jest.fn()}
deleteApplication={jest.fn()} deleteApplication={jest.fn()}
history={{}} history={{}}
application={{ application={{
appName: 'test-app', appName: 'test-app',
instances: [ instances: [
{ {
instanceId: 'instance-1', instanceId: 'instance-1',
clientIp: '123.123.123.123', clientIp: '123.123.123.123',
lastSeen: '2017-02-23T15:56:49', lastSeen: '2017-02-23T15:56:49',
sdkVersion: '4.0', sdkVersion: '4.0',
}, },
], ],
strategies: [ strategies: [
{ {
name: 'StrategyA', name: 'StrategyA',
description: 'A description', description: 'A description',
}, },
{ {
name: 'StrategyB', name: 'StrategyB',
description: 'B description', description: 'B description',
notFound: true, notFound: true,
}, },
], ],
seenToggles: [ seenToggles: [
{ {
name: 'ToggleA', name: 'ToggleA',
description: 'this is A toggle', description: 'this is A toggle',
enabled: true, enabled: true,
}, },
{ {
name: 'ToggleB', name: 'ToggleB',
description: 'this is B toggle', description: 'this is B toggle',
enabled: false, enabled: false,
notFound: true, notFound: true,
}, },
], ],
url: 'http://example.org', url: 'http://example.org',
description: 'app description', description: 'app description',
}} }}
location={{ locale: 'en-GB' }} location={{ locale: 'en-GB' }}
/> />
</AccessProvider> </AccessProvider>
</ThemeProvider> </ThemeProvider>
</MemoryRouter> </MemoryRouter>
@ -91,51 +96,53 @@ test('renders correctly with permissions', () => {
.create( .create(
<MemoryRouter> <MemoryRouter>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<AccessProvider store={createFakeStore([{permission: ADMIN}])}> <AccessProvider
<ClientApplications store={createFakeStore([{ permission: ADMIN }])}
fetchApplication={() => Promise.resolve({})} >
storeApplicationMetaData={jest.fn()} <ClientApplications
history={{}} fetchApplication={() => Promise.resolve({})}
deleteApplication={jest.fn()} storeApplicationMetaData={jest.fn()}
application={{ history={{}}
appName: 'test-app', deleteApplication={jest.fn()}
instances: [ application={{
{ appName: 'test-app',
instanceId: 'instance-1', instances: [
clientIp: '123.123.123.123', {
lastSeen: '2017-02-23T15:56:49', instanceId: 'instance-1',
sdkVersion: '4.0', clientIp: '123.123.123.123',
}, lastSeen: '2017-02-23T15:56:49',
], sdkVersion: '4.0',
strategies: [ },
{ ],
name: 'StrategyA', strategies: [
description: 'A description', {
}, name: 'StrategyA',
{ description: 'A description',
name: 'StrategyB', },
description: 'B description', {
notFound: true, name: 'StrategyB',
}, description: 'B description',
], notFound: true,
seenToggles: [ },
{ ],
name: 'ToggleA', seenToggles: [
description: 'this is A toggle', {
enabled: true, name: 'ToggleA',
}, description: 'this is A toggle',
{ enabled: true,
name: 'ToggleB', },
description: 'this is B toggle', {
enabled: false, name: 'ToggleB',
notFound: true, description: 'this is B toggle',
}, enabled: false,
], notFound: true,
url: 'http://example.org', },
description: 'app description', ],
}} url: 'http://example.org',
location={{ locale: 'en-GB' }} description: 'app description',
/> }}
location={{ locale: 'en-GB' }}
/>
</AccessProvider> </AccessProvider>
</ThemeProvider> </ThemeProvider>
</MemoryRouter> </MemoryRouter>

View File

@ -2,9 +2,20 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; 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 ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
import { formatFullDateTimeWithLocale, formatDateWithLocale } from '../common/util'; import {
formatFullDateTimeWithLocale,
formatDateWithLocale,
} from '../common/util';
import { UPDATE_APPLICATION } from '../AccessProvider/permissions'; import { UPDATE_APPLICATION } from '../AccessProvider/permissions';
import ApplicationView from './application-view'; import ApplicationView from './application-view';
import ApplicationUpdate from './application-update'; import ApplicationUpdate from './application-update';
@ -37,9 +48,12 @@ class ClientApplications extends PureComponent {
} }
componentDidMount() { 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); formatDate = v => formatDateWithLocale(v, this.props.location.locale);
deleteApplication = async evt => { deleteApplication = async evt => {
@ -64,7 +78,16 @@ class ClientApplications extends PureComponent {
} }
const { hasAccess } = this.context; const { hasAccess } = this.context;
const { application, storeApplicationMetaData } = this.props; 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 = () => { const toggleModal = () => {
this.setState(prev => ({ ...prev, prompt: !prev.prompt })); this.setState(prev => ({ ...prev, prompt: !prev.prompt }));
@ -95,7 +118,10 @@ class ClientApplications extends PureComponent {
{ {
label: 'Edit application', label: 'Edit application',
component: ( component: (
<ApplicationUpdate application={application} storeApplicationMetaData={storeApplicationMetaData} /> <ApplicationUpdate
application={application}
storeApplicationMetaData={storeApplicationMetaData}
/>
), ),
}, },
]; ];
@ -131,7 +157,11 @@ class ClientApplications extends PureComponent {
<ConditionallyRender <ConditionallyRender
condition={hasAccess(UPDATE_APPLICATION)} condition={hasAccess(UPDATE_APPLICATION)}
show={ show={
<Button color="secondary" title="Delete application" onClick={toggleModal}> <Button
color="secondary"
title="Delete application"
onClick={toggleModal}
>
Delete Delete
</Button> </Button>
} }

View File

@ -1,12 +1,27 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import PropTypes from 'prop-types'; 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 { shorten } from '../common';
import { CREATE_FEATURE, CREATE_STRATEGY } from '../AccessProvider/permissions'; import { CREATE_FEATURE, CREATE_STRATEGY } from '../AccessProvider/permissions';
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender'; 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 }) => ( const notFoundListItem = ({ createUrl, name, permission }) => (
<ConditionallyRender <ConditionallyRender
key={`not_found_conditional_${name}`} key={`not_found_conditional_${name}`}
@ -17,7 +32,9 @@ function ApplicationView({ seenToggles, hasAccess, strategies, instances, format
<Icon>report</Icon> <Icon>report</Icon>
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={<Link to={`${createUrl}?name=${name}`}>{name}</Link>} primary={
<Link to={`${createUrl}?name=${name}`}>{name}</Link>
}
secondary={'Missing, want to create?'} secondary={'Missing, want to create?'}
/> />
</ListItem> </ListItem>
@ -27,14 +44,24 @@ function ApplicationView({ seenToggles, hasAccess, strategies, instances, format
<ListItemAvatar> <ListItemAvatar>
<Icon>report</Icon> <Icon>report</Icon>
</ListItemAvatar> </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> </ListItem>
} }
/> />
); );
// eslint-disable-next-line react/prop-types // 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}`}> <ListItem key={`found_${name}-${i}`}>
<ListItemAvatar> <ListItemAvatar>
<ConditionallyRender <ConditionallyRender
@ -45,7 +72,9 @@ function ApplicationView({ seenToggles, hasAccess, strategies, instances, format
/> />
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={<Link to={`${viewUrl}/${name}`}>{shorten(name, 50)}</Link>} primary={
<Link to={`${viewUrl}/${name}`}>{shorten(name, 50)}</Link>
}
secondary={shorten(description, 60)} secondary={shorten(description, 60)}
/> />
</ListItem> </ListItem>
@ -58,26 +87,28 @@ function ApplicationView({ seenToggles, hasAccess, strategies, instances, format
</Typography> </Typography>
<hr /> <hr />
<List> <List>
{seenToggles.map(({ name, description, enabled, notFound }, i) => ( {seenToggles.map(
<ConditionallyRender ({ name, description, enabled, notFound }, i) => (
key={`toggle_conditional_${name}`} <ConditionallyRender
condition={notFound} key={`toggle_conditional_${name}`}
show={notFoundListItem({ condition={notFound}
createUrl: '/features/create', show={notFoundListItem({
name, createUrl: '/features/create',
permission: CREATE_FEATURE, name,
i, permission: CREATE_FEATURE,
})} i,
elseShow={foundListItem({ })}
viewUrl: '/features/strategies', elseShow={foundListItem({
name, viewUrl: '/features/strategies',
showSwitch: true, name,
enabled, showSwitch: true,
description, enabled,
i, description,
})} i,
/> })}
))} />
)
)}
</List> </List>
</Grid> </Grid>
<Grid item xl={6} md={6} xs={12}> <Grid item xl={6} md={6} xs={12}>
@ -114,28 +145,33 @@ function ApplicationView({ seenToggles, hasAccess, strategies, instances, format
</Typography> </Typography>
<hr /> <hr />
<List> <List>
{instances.map(({ instanceId, clientIp, lastSeen, sdkVersion }) => ( {instances.map(
<ListItem key={`${instanceId}`}> ({ instanceId, clientIp, lastSeen, sdkVersion }) => (
<ListItemAvatar> <ListItem key={`${instanceId}`}>
<Icon>timeline</Icon> <ListItemAvatar>
</ListItemAvatar> <Icon>timeline</Icon>
<ListItemText </ListItemAvatar>
primary={ <ListItemText
<ConditionallyRender primary={
key={`${instanceId}_conditional`} <ConditionallyRender
condition={sdkVersion} key={`${instanceId}_conditional`}
show={`${instanceId} (${sdkVersion})`} condition={sdkVersion}
elseShow={instanceId} show={`${instanceId} (${sdkVersion})`}
/> elseShow={instanceId}
} />
secondary={ }
<span> secondary={
{clientIp} last seen at <small>{formatFullDateTime(lastSeen)}</small> <span>
</span> {clientIp} last seen at{' '}
} <small>
/> {formatFullDateTime(lastSeen)}
</ListItem> </small>
))} </span>
}
/>
</ListItem>
)
)}
</List> </List>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -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',
},
});

View 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;

View File

@ -11,7 +11,7 @@ const ProtectedRoute = ({
{...rest} {...rest}
render={props => { render={props => {
if (unauthorized) { if (unauthorized) {
return <Redirect to="/login" />; return <Redirect to={'/login'} />;
} else { } else {
return <Component {...props} {...renderProps} />; return <Component {...props} {...renderProps} />;
} }

View File

@ -2,9 +2,20 @@ import PropTypes from 'prop-types';
import PageContent from '../../common/PageContent/PageContent'; import PageContent from '../../common/PageContent/PageContent';
import HeaderTitle from '../../common/HeaderTitle'; import HeaderTitle from '../../common/HeaderTitle';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import { CREATE_CONTEXT_FIELD, DELETE_CONTEXT_FIELD } from '../../AccessProvider/permissions'; import {
import { Icon, IconButton, List, ListItem, ListItemIcon, ListItemText, Tooltip } from '@material-ui/core'; CREATE_CONTEXT_FIELD,
import React, { useContext, useState } from 'react'; 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 { Link } from 'react-router-dom';
import { useStyles } from './styles'; import { useStyles } from './styles';
import ConfirmDialogue from '../../common/Dialogue'; import ConfirmDialogue from '../../common/Dialogue';
@ -61,7 +72,14 @@ const ContextList = ({ removeContextField, history, contextFields }) => {
/> />
); );
return ( return (
<PageContent headerContent={<HeaderTitle actions={headerButton()} title={'Context fields'} />}> <PageContent
headerContent={
<HeaderTitle
actions={headerButton()}
title={'Context fields'}
/>
}
>
<List> <List>
<ConditionallyRender <ConditionallyRender
condition={contextFields.length > 0} condition={contextFields.length > 0}

View File

@ -282,7 +282,6 @@ class AddContextComponent extends Component {
Read more Read more
</a> </a>
</p> </p>
{console.log(contextField.stickiness)}
<Switch <Switch
label="Allow stickiness" label="Allow stickiness"
checked={contextField.stickiness} checked={contextField.stickiness}

View File

@ -27,7 +27,15 @@ const FeatureToggleListItem = ({
}) => { }) => {
const styles = useStyles(); 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 { showLastHour = false } = settings;
const isStale = showLastHour const isStale = showLastHour
? metricsLastHour.isFallback ? metricsLastHour.isFallback

View File

@ -9,8 +9,6 @@ import { createFakeStore } from '../../../../accessStoreFake';
import { ADMIN, CREATE_FEATURE } from '../../../AccessProvider/permissions'; import { ADMIN, CREATE_FEATURE } from '../../../AccessProvider/permissions';
import AccessProvider from '../../../AccessProvider/AccessProvider'; import AccessProvider from '../../../AccessProvider/AccessProvider';
jest.mock('../FeatureToggleListItem', () => ({ jest.mock('../FeatureToggleListItem', () => ({
__esModule: true, __esModule: true,
default: 'ListItem', default: 'ListItem',
@ -29,17 +27,19 @@ test('renders correctly with one feature', () => {
const tree = renderer.create( const tree = renderer.create(
<MemoryRouter> <MemoryRouter>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<AccessProvider store={createFakeStore([{permission: CREATE_FEATURE}])}> <AccessProvider
<FeatureToggleList store={createFakeStore([{ permission: CREATE_FEATURE }])}
updateSetting={jest.fn()} >
settings={settings} <FeatureToggleList
history={{}} updateSetting={jest.fn()}
featureMetrics={featureMetrics} settings={settings}
features={features} history={{}}
toggleFeature={jest.fn()} featureMetrics={featureMetrics}
fetcher={jest.fn()} features={features}
currentProjectId='default' toggleFeature={jest.fn()}
/> fetcher={jest.fn()}
currentProjectId="default"
/>
</AccessProvider> </AccessProvider>
</ThemeProvider> </ThemeProvider>
</MemoryRouter> </MemoryRouter>
@ -59,17 +59,19 @@ test('renders correctly with one feature without permissions', () => {
const tree = renderer.create( const tree = renderer.create(
<MemoryRouter> <MemoryRouter>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<AccessProvider store={createFakeStore([{permission: CREATE_FEATURE}])}> <AccessProvider
<FeatureToggleList store={createFakeStore([{ permission: CREATE_FEATURE }])}
updateSetting={jest.fn()} >
settings={settings} <FeatureToggleList
history={{}} updateSetting={jest.fn()}
featureMetrics={featureMetrics} settings={settings}
features={features} history={{}}
toggleFeature={jest.fn()} featureMetrics={featureMetrics}
fetcher={jest.fn()} features={features}
currentProjectId='default' toggleFeature={jest.fn()}
/> fetcher={jest.fn()}
currentProjectId="default"
/>
</AccessProvider> </AccessProvider>
</ThemeProvider> </ThemeProvider>
</MemoryRouter> </MemoryRouter>

View File

@ -58,7 +58,7 @@ const FeatureView = ({
const [delDialog, setDelDialog] = useState(false); const [delDialog, setDelDialog] = useState(false);
const commonStyles = useCommonStyles(); const commonStyles = useCommonStyles();
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const { project } = featureToggle || { }; const { project } = featureToggle || {};
useEffect(() => { useEffect(() => {
scrollToTop(); scrollToTop();
@ -82,12 +82,14 @@ const FeatureView = ({
const getTabComponent = key => { const getTabComponent = key => {
switch (key) { switch (key) {
case 'activation': case 'activation':
return <UpdateStrategies return (
<UpdateStrategies
featureToggle={featureToggle} featureToggle={featureToggle}
features={features} features={features}
history={history} history={history}
editable={editable} editable={editable}
/> />
);
case 'metrics': case 'metrics':
return <MetricComponent featureToggle={featureToggle} />; return <MetricComponent featureToggle={featureToggle} />;
case 'variants': case 'variants':

View File

@ -3,7 +3,14 @@ import PropTypes from 'prop-types';
import { Link } from 'react-router-dom'; 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 as commonStyles } from '../../common';
import styles from './copy-feature-component.module.scss'; 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() { render() {
@ -86,17 +97,23 @@ class CopyFeatureComponent extends Component {
const { newToggleName, nameError, replaceGroupId } = this.state; const { newToggleName, nameError, replaceGroupId } = this.state;
return ( return (
<Paper className={commonStyles.fullwidth} style={{ overflow: 'visible' }}> <Paper
className={commonStyles.fullwidth}
style={{ overflow: 'visible' }}
>
<div className={styles.header}> <div className={styles.header}>
<h1>Copy&nbsp;{copyToggle.name}</h1> <h1>Copy&nbsp;{copyToggle.name}</h1>
</div> </div>
<section className={styles.content}> <section className={styles.content}>
<p className={styles.text}> <p className={styles.text}>
You are about to create a new feature toggle by cloning the configuration of feature You are about to create a new feature toggle by cloning
toggle&nbsp; the configuration of feature toggle&nbsp;
<Link to={`/features/strategies/${copyToggle.name}`}>{copyToggle.name}</Link>. You must give the <Link to={`/features/strategies/${copyToggle.name}`}>
new feature toggle a unique name before you can proceed. {copyToggle.name}
</Link>
. You must give the new feature toggle a unique name
before you can proceed.
</p> </p>
<form onSubmit={this.onSubmit}> <form onSubmit={this.onSubmit}>
<TextField <TextField
@ -123,7 +140,11 @@ class CopyFeatureComponent extends Component {
label="Replace groupId" label="Replace groupId"
/> />
<Button type="submit" color="primary" variant="contained"> <Button
type="submit"
color="primary"
variant="contained"
>
<Icon>file_copy</Icon> <Icon>file_copy</Icon>
&nbsp;&nbsp;&nbsp; Create from copy &nbsp;&nbsp;&nbsp; Create from copy
</Button> </Button>

View File

@ -3,7 +3,18 @@ import PropTypes from 'prop-types';
import { Icon, Chip } from '@material-ui/core'; import { Icon, Chip } from '@material-ui/core';
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
import Dialogue from '../common/Dialogue'; 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 [showDialog, setShowDialog] = useState(false);
const [selectedTag, setSelectedTag] = useState(undefined); const [selectedTag, setSelectedTag] = useState(undefined);
const onUntagFeature = tag => { const onUntagFeature = tag => {
@ -20,11 +31,29 @@ function FeatureTagComponent({ tags, tagTypes, featureToggleName, untagFeature }
if (tagType && tagType.icon) { if (tagType && tagType.icon) {
switch (tagType.name) { switch (tagType.name) {
case 'slack': case 'slack':
return <img style={style} alt="slack" src="slack.svg" />; return (
<img
style={style}
alt="slack"
src={formatAssetPath(slackIcon)}
/>
);
case 'jira': case 'jira':
return <img style={style} alt="jira" src="jira.svg" />; return (
<img
style={style}
alt="jira"
src={formatAssetPath(jiraIcon)}
/>
);
case 'webhook': case 'webhook':
return <img style={style} alt="webhook" src="webhooks.svg" />; return (
<img
style={style}
alt="webhook"
src={formatAssetPath(webhookIcon)}
/>
);
default: default:
return <Icon>label</Icon>; return <Icon>label</Icon>;
} }

View File

@ -5,7 +5,7 @@ import MySelect from '../common/select';
class FeatureTypeSelectComponent extends Component { class FeatureTypeSelectComponent extends Component {
componentDidMount() { componentDidMount() {
const { fetchFeatureTypes, types } = this.props; const { fetchFeatureTypes, types } = this.props;
if (types[0].initial && fetchFeatureTypes) { if (types && types[0].initial && fetchFeatureTypes) {
this.props.fetchFeatureTypes(); this.props.fetchFeatureTypes();
} }
} }
@ -33,7 +33,17 @@ class FeatureTypeSelectComponent extends Component {
options.push({ key: value, label: value }); 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}
/>
);
} }
} }

View File

@ -7,7 +7,11 @@ import { Provider } from 'react-redux';
import { ThemeProvider } from '@material-ui/core'; import { ThemeProvider } from '@material-ui/core';
import ViewFeatureToggleComponent from '../../FeatureView/FeatureView'; import ViewFeatureToggleComponent from '../../FeatureView/FeatureView';
import renderer from 'react-test-renderer'; 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 theme from '../../../../themes/main-theme';
import { createFakeStore } from '../../../../accessStoreFake'; import { createFakeStore } from '../../../../accessStoreFake';
@ -63,18 +67,20 @@ test('renders correctly with one feature', () => {
<MemoryRouter> <MemoryRouter>
<Provider store={createStore(mockReducer, mockStore)}> <Provider store={createStore(mockReducer, mockStore)}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<AccessProvider store={createFakeStore([{permission: ADMIN}])}> <AccessProvider
<ViewFeatureToggleComponent store={createFakeStore([{ permission: ADMIN }])}
activeTab={'strategies'} >
featureToggleName="another" <ViewFeatureToggleComponent
features={[feature]} activeTab={'strategies'}
featureToggle={feature} featureToggleName="another"
fetchFeatureToggles={jest.fn()} features={[feature]}
history={{}} featureToggle={feature}
featureTags={[]} fetchFeatureToggles={jest.fn()}
fetchTags={jest.fn()} history={{}}
untagFeature={jest.fn()} featureTags={[]}
/> fetchTags={jest.fn()}
untagFeature={jest.fn()}
/>
</AccessProvider> </AccessProvider>
</ThemeProvider> </ThemeProvider>
</Provider> </Provider>

View File

@ -1,5 +1,5 @@
import ConditionallyRender from '../../common/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender';
import MainLayout from '../MainLayout/MainLayout'; import MainLayout from '../MainLayout';
const LayoutPicker = ({ children, location }) => { const LayoutPicker = ({ children, location }) => {
const standalonePages = () => { const standalonePages = () => {

View File

@ -8,6 +8,7 @@ import styles from '../../styles.module.scss';
import ErrorContainer from '../../error/error-container'; import ErrorContainer from '../../error/error-container';
import Header from '../../menu/Header'; import Header from '../../menu/Header';
import Footer from '../../menu/Footer/Footer'; import Footer from '../../menu/Footer/Footer';
import Proclamation from '../../common/Proclamation/Proclamation';
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
container: { container: {
@ -16,7 +17,7 @@ const useStyles = makeStyles(theme => ({
}, },
})); }));
const MainLayout = ({ children, location }) => { const MainLayout = ({ children, location, uiConfig }) => {
const muiStyles = useStyles(); const muiStyles = useStyles();
return ( return (
@ -26,6 +27,7 @@ const MainLayout = ({ children, location }) => {
<div className={classnames(styles.contentWrapper)}> <div className={classnames(styles.contentWrapper)}>
<Grid item className={styles.content} xs={12} sm={12}> <Grid item className={styles.content} xs={12} sm={12}>
<div className={styles.contentContainer}> <div className={styles.contentContainer}>
<Proclamation toast={uiConfig.toast} />
{children} {children}
</div> </div>
<ErrorContainer /> <ErrorContainer />

View 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);

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import useMediaQuery from '@material-ui/core/useMediaQuery'; import useMediaQuery from '@material-ui/core/useMediaQuery';
import { useTheme } from '@material-ui/core/styles'; 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 MenuBookIcon from '@material-ui/icons/MenuBook';
import { useStyles } from './styles'; import { useStyles } from './styles';
const Header = ({ uiConfig, init }) => { const Header = ({ uiConfig }) => {
const theme = useTheme(); const theme = useTheme();
const smallScreen = useMediaQuery(theme.breakpoints.down('sm')); const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const styles = useStyles(); const styles = useStyles();
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
useEffect(() => {
init(uiConfig.flags);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const toggleDrawer = () => setOpenDrawer(prev => !prev); const toggleDrawer = () => setOpenDrawer(prev => !prev);
const { links, name, flags } = uiConfig; const { links, name, flags } = uiConfig;
@ -83,7 +78,6 @@ const Header = ({ uiConfig, init }) => {
Header.propTypes = { Header.propTypes = {
uiConfig: PropTypes.object.isRequired, uiConfig: PropTypes.object.isRequired,
init: PropTypes.func.isRequired,
location: PropTypes.object.isRequired, location: PropTypes.object.isRequired,
}; };

View File

@ -1,10 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { loadInitialData } from '../../../store/loader';
import Header from './Header'; import Header from './Header';
const mapStateToProps = state => ({ uiConfig: state.uiConfig.toJS() }); const mapStateToProps = state => ({ uiConfig: state.uiConfig.toJS() });
export default connect(mapStateToProps, { export default connect(mapStateToProps)(Header);
init: loadInitialData,
})(Header);

View File

@ -33,8 +33,12 @@ const renderRoute = (params, route) => {
if (!route) { if (!route) {
return null; return null;
} }
const title = route.title.startsWith(':') ? params[route.title.substring(1)] : route.title; const title = route.title.startsWith(':')
return route.parent ? renderDoubleBread(title, getRoute(route.parent)) : renderBread(route); ? params[route.title.substring(1)]
: route.title;
return route.parent
? renderDoubleBread(title, getRoute(route.parent))
: renderBread(route);
}; };
/* /*
@ -53,7 +57,9 @@ const Breadcrumb = () => (
<Route <Route
key={route.path} key={route.path}
path={route.path} path={route.path}
render={({ match: { params } } = this.props) => renderRoute(params, route)} render={({ match: { params } } = this.props) =>
renderRoute(params, route)
}
/> />
))} ))}
</Switch> </Switch>

View File

@ -8,6 +8,8 @@ import styles from './drawer.module.scss';
import { baseRoutes as routes } from './routes'; import { baseRoutes as routes } from './routes';
import logo from '../../assets/img/logo.png';
const filterByFlags = flags => r => { const filterByFlags = flags => r => {
if (r.flag && !flags[r.flag]) { if (r.flag && !flags[r.flag]) {
return false; return false;
@ -17,7 +19,15 @@ const filterByFlags = flags => r => {
function getIcon(name) { function getIcon(name) {
if (name === 'c_github') { 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 { } else {
return <Icon className={styles.navigationIcon}>{name}</Icon>; return <Icon className={styles.navigationIcon}>{name}</Icon>;
} }
@ -43,7 +53,8 @@ function renderLink(link, toggleDrawer) {
key={link.href} key={link.href}
target="_blank" target="_blank"
className={[styles.navigationLink].join(' ')} className={[styles.navigationLink].join(' ')}
title={link.title} rel="noreferrer" title={link.title}
rel="noreferrer"
> >
{getIcon(link.icon)} {link.value} {getIcon(link.icon)} {link.value}
</a> </a>
@ -51,12 +62,29 @@ function renderLink(link, toggleDrawer) {
} }
} }
export const DrawerMenu = ({ links = [], title = 'Unleash', flags = {}, open = false, toggleDrawer }) => ( export const DrawerMenu = ({
<Drawer className={styles.drawer} open={open} anchor={'left'} onClose={() => toggleDrawer()}> links = [],
title = 'Unleash',
flags = {},
open = false,
toggleDrawer,
}) => (
<Drawer
className={styles.drawer}
open={open}
anchor={'left'}
onClose={() => toggleDrawer()}
>
<div className={styles.drawerContainer}> <div className={styles.drawerContainer}>
<div className={styles.drawerTitleContainer}> <div className={styles.drawerTitleContainer}>
<span className={[styles.drawerTitle].join(' ')}> <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 className={styles.drawerTitleText}>{title}</span>
</span> </span>
</div> </div>
@ -68,7 +96,9 @@ export const DrawerMenu = ({ links = [], title = 'Unleash', flags = {}, open = f
key={item.path} key={item.path}
to={item.path} to={item.path}
className={classnames(styles.navigationLink)} className={classnames(styles.navigationLink)}
activeClassName={classnames(styles.navigationLinkActive)} activeClassName={classnames(
styles.navigationLinkActive
)}
> >
{getIcon(item.icon)} {getIcon(item.icon)}
{item.title} {item.title}
@ -76,7 +106,9 @@ export const DrawerMenu = ({ links = [], title = 'Unleash', flags = {}, open = f
))} ))}
</List> </List>
<Divider /> <Divider />
<List className={styles.navigation}>{links.map(l => renderLink(l, toggleDrawer))}</List> <List className={styles.navigation}>
{links.map(l => renderLink(l, toggleDrawer))}
</List>
</div> </div>
</Drawer> </Drawer>
); );

View File

@ -2,8 +2,20 @@ import PropTypes from 'prop-types';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import HeaderTitle from '../../common/HeaderTitle'; import HeaderTitle from '../../common/HeaderTitle';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import { CREATE_PROJECT, DELETE_PROJECT, UPDATE_PROJECT } from '../../AccessProvider/permissions'; import {
import { Icon, IconButton, List, ListItem, ListItemAvatar, ListItemText, Tooltip } from '@material-ui/core'; 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 { Link } from 'react-router-dom';
import ConfirmDialogue from '../../common/Dialogue'; import ConfirmDialogue from '../../common/Dialogue';
import PageContent from '../../common/PageContent/PageContent'; import PageContent from '../../common/PageContent/PageContent';
@ -24,7 +36,10 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history }) => {
condition={hasAccess(CREATE_PROJECT)} condition={hasAccess(CREATE_PROJECT)}
show={ show={
<Tooltip title="Add new project"> <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> <Icon>add</Icon>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@ -40,8 +55,11 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history }) => {
const mgmAccessButton = project => ( const mgmAccessButton = project => (
<Tooltip title="Manage access"> <Tooltip title="Manage access">
<Link to={`/projects/${project.id}/access`} style={{ color: 'black' }}> <Link
<IconButton aria-label="manage_access" > to={`/projects/${project.id}/access`}
style={{ color: 'black' }}
>
<IconButton aria-label="manage_access">
<Icon>supervised_user_circle</Icon> <Icon>supervised_user_circle</Icon>
</IconButton> </IconButton>
</Link> </Link>
@ -68,17 +86,27 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history }) => {
<ListItemAvatar> <ListItemAvatar>
<Icon>folder_open</Icon> <Icon>folder_open</Icon>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary={projectLink(project)} secondary={project.description} /> <ListItemText
primary={projectLink(project)}
secondary={project.description}
/>
<ConditionallyRender <ConditionallyRender
condition={hasAccess(UPDATE_PROJECT, project.name)} condition={hasAccess(UPDATE_PROJECT, project.name)}
show={mgmAccessButton(project)} show={mgmAccessButton(project)}
/> />
<ConditionallyRender condition={hasAccess(DELETE_PROJECT, project.name)} show={deleteProjectButton(project)} /> <ConditionallyRender
condition={hasAccess(DELETE_PROJECT, project.name)}
show={deleteProjectButton(project)}
/>
</ListItem> </ListItem>
)); ));
return ( return (
<PageContent headerContent={<HeaderTitle title="Projects" actions={addProjectButton()} />}> <PageContent
headerContent={
<HeaderTitle title="Projects" actions={addProjectButton()} />
}
>
<List> <List>
<ConditionallyRender <ConditionallyRender
condition={projects.length > 0} condition={projects.length > 0}

View File

@ -42,15 +42,17 @@ test('renders correctly with one strategy without permissions', () => {
const tree = renderer.create( const tree = renderer.create(
<MemoryRouter> <MemoryRouter>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<AccessProvider store={createFakeStore([{permission: ADMIN}])}> <AccessProvider
<StrategiesListComponent store={createFakeStore([{ permission: ADMIN }])}
strategies={[strategy]} >
fetchStrategies={jest.fn()} <StrategiesListComponent
removeStrategy={jest.fn()} strategies={[strategy]}
deprecateStrategy={jest.fn()} fetchStrategies={jest.fn()}
reactivateStrategy={jest.fn()} removeStrategy={jest.fn()}
history={{}} deprecateStrategy={jest.fn()}
/> reactivateStrategy={jest.fn()}
history={{}}
/>
</AccessProvider> </AccessProvider>
</ThemeProvider> </ThemeProvider>
</MemoryRouter> </MemoryRouter>

View File

@ -36,7 +36,7 @@ test('renders correctly with one strategy', () => {
const tree = renderer.create( const tree = renderer.create(
<MemoryRouter> <MemoryRouter>
<AccessProvider store={createFakeStore()}> <AccessProvider store={createFakeStore()}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<StrategyDetails <StrategyDetails
strategyName={'Another'} strategyName={'Another'}
strategy={strategy} strategy={strategy}
@ -48,7 +48,7 @@ test('renders correctly with one strategy', () => {
fetchFeatureToggles={jest.fn()} fetchFeatureToggles={jest.fn()}
history={{}} history={{}}
/> />
</ThemeProvider> </ThemeProvider>
</AccessProvider> </AccessProvider>
</MemoryRouter> </MemoryRouter>
); );

View File

@ -51,7 +51,13 @@ export default class StrategyDetails extends Component {
}, },
{ {
label: 'Edit', 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}> <PageContent headerContent={strategy.name}>
<Grid container> <Grid container>
<Grid item xs={12} sm={12}> <Grid item xs={12} sm={12}>
<Typography variant="subtitle1">{strategy.description}</Typography> <Typography variant="subtitle1">
{strategy.description}
</Typography>
<ConditionallyRender <ConditionallyRender
condition={strategy.editable && hasAccess(UPDATE_STRATEGY)} condition={
strategy.editable && hasAccess(UPDATE_STRATEGY)
}
show={ show={
<div> <div>
<TabNav tabData={tabData} /> <TabNav tabData={tabData} />
@ -75,7 +85,9 @@ export default class StrategyDetails extends Component {
<ShowStrategy <ShowStrategy
strategy={this.props.strategy} strategy={this.props.strategy}
toggles={this.props.toggles} toggles={this.props.toggles}
applications={this.props.applications} applications={
this.props.applications
}
/> />
</div> </div>
</section> </section>

View File

@ -16,7 +16,10 @@ import {
import HeaderTitle from '../../common/HeaderTitle'; import HeaderTitle from '../../common/HeaderTitle';
import PageContent from '../../common/PageContent/PageContent'; import PageContent from '../../common/PageContent/PageContent';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; 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 Dialogue from '../../common/Dialogue/Dialogue';
import useMediaQuery from '@material-ui/core/useMediaQuery'; import useMediaQuery from '@material-ui/core/useMediaQuery';
@ -29,7 +32,6 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
const history = useHistory(); const history = useHistory();
const smallScreen = useMediaQuery('(max-width:700px)'); const smallScreen = useMediaQuery('(max-width:700px)');
useEffect(() => { useEffect(() => {
fetchTagTypes(); fetchTagTypes();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -48,7 +50,9 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
<Tooltip title="Add tag type"> <Tooltip title="Add tag type">
<IconButton <IconButton
aria-label="add tag type" aria-label="add tag type"
onClick={() => history.push('/tag-types/create')} onClick={() =>
history.push('/tag-types/create')
}
> >
<Icon>add</Icon> <Icon>add</Icon>
</IconButton> </IconButton>
@ -58,7 +62,9 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
onClick={() => history.push('/tag-types/create')} onClick={() =>
history.push('/tag-types/create')
}
> >
Add new tag type Add new tag type
</Button> </Button>
@ -91,12 +97,18 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
</Tooltip> </Tooltip>
); );
return ( return (
<ListItem key={`${tagType.name}`} classes={{ root: styles.tagListItem }}> <ListItem
key={`${tagType.name}`}
classes={{ root: styles.tagListItem }}
>
<ListItemIcon> <ListItemIcon>
<Icon>label</Icon> <Icon>label</Icon>
</ListItemIcon> </ListItemIcon>
<ListItemText primary={link} secondary={tagType.description} /> <ListItemText primary={link} secondary={tagType.description} />
<ConditionallyRender condition={hasAccess(DELETE_TAG_TYPE)} show={deleteButton} /> <ConditionallyRender
condition={hasAccess(DELETE_TAG_TYPE)}
show={deleteButton}
/>
</ListItem> </ListItem>
); );
}; };

View File

@ -5,7 +5,10 @@ import renderer from 'react-test-renderer';
import theme from '../../../themes/main-theme'; import theme from '../../../themes/main-theme';
import AccessProvider from '../../AccessProvider/AccessProvider'; import AccessProvider from '../../AccessProvider/AccessProvider';
import { createFakeStore } from '../../../accessStoreFake'; 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'); jest.mock('@material-ui/core/TextField');
@ -13,16 +16,18 @@ test('renders correctly for creating', () => {
const tree = renderer const tree = renderer
.create( .create(
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<AccessProvider store={createFakeStore([{permission: CREATE_TAG_TYPE}])}> <AccessProvider
<TagTypes store={createFakeStore([{ permission: CREATE_TAG_TYPE }])}
history={{}} >
title="Add tag type" <TagTypes
createTagType={jest.fn()} history={{}}
validateName={() => Promise.resolve(true)} title="Add tag type"
tagType={{ name: '', description: '', icon: '' }} createTagType={jest.fn()}
editMode={false} validateName={() => Promise.resolve(true)}
submit={jest.fn()} tagType={{ name: '', description: '', icon: '' }}
/> editMode={false}
submit={jest.fn()}
/>
</AccessProvider> </AccessProvider>
</ThemeProvider> </ThemeProvider>
) )
@ -35,15 +40,15 @@ test('renders correctly for creating without permissions', () => {
.create( .create(
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<AccessProvider store={createFakeStore([])}> <AccessProvider store={createFakeStore([])}>
<TagTypes <TagTypes
history={{}} history={{}}
title="Add tag type" title="Add tag type"
createTagType={jest.fn()} createTagType={jest.fn()}
validateName={() => Promise.resolve(true)} validateName={() => Promise.resolve(true)}
tagType={{ name: '', description: '', icon: '' }} tagType={{ name: '', description: '', icon: '' }}
editMode={false} editMode={false}
submit={jest.fn()} submit={jest.fn()}
/> />
</AccessProvider> </AccessProvider>
</ThemeProvider> </ThemeProvider>
) )
@ -55,16 +60,18 @@ test('it supports editMode', () => {
const tree = renderer const tree = renderer
.create( .create(
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<AccessProvider store={createFakeStore([{permission: UPDATE_TAG_TYPE}])}> <AccessProvider
<TagTypes store={createFakeStore([{ permission: UPDATE_TAG_TYPE }])}
history={{}} >
title="Add tag type" <TagTypes
createTagType={jest.fn()} history={{}}
validateName={() => Promise.resolve(true)} title="Add tag type"
tagType={{ name: '', description: '', icon: '' }} createTagType={jest.fn()}
editMode validateName={() => Promise.resolve(true)}
submit={jest.fn()} tagType={{ name: '', description: '', icon: '' }}
/> editMode
submit={jest.fn()}
/>
</AccessProvider> </AccessProvider>
</ThemeProvider> </ThemeProvider>
) )

View File

@ -8,15 +8,20 @@ import theme from '../../../themes/main-theme';
import { createFakeStore } from '../../../accessStoreFake'; import { createFakeStore } from '../../../accessStoreFake';
import AccessProvider from '../../AccessProvider/AccessProvider'; 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', () => { test('renders an empty list correctly', () => {
const tree = renderer.create( const tree = renderer.create(
<MemoryRouter> <MemoryRouter>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<AccessProvider store={createFakeStore([{permission: ADMIN }])}> <AccessProvider
store={createFakeStore([{ permission: ADMIN }])}
>
<TagTypesList <TagTypesList
tagTypes={[]} tagTypes={[]}
fetchTagTypes={jest.fn()} fetchTagTypes={jest.fn()}
@ -34,11 +39,13 @@ test('renders a list with elements correctly', () => {
const tree = renderer.create( const tree = renderer.create(
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<MemoryRouter> <MemoryRouter>
<AccessProvider store={createFakeStore([ <AccessProvider
{permission: CREATE_TAG_TYPE }, store={createFakeStore([
{permission: UPDATE_TAG_TYPE }, { permission: CREATE_TAG_TYPE },
{permission: DELETE_TAG_TYPE } { permission: UPDATE_TAG_TYPE },
])}> { permission: DELETE_TAG_TYPE },
])}
>
<TagTypesList <TagTypesList
tagTypes={[ tagTypes={[
{ {

View File

@ -9,12 +9,23 @@ import { Typography, TextField } from '@material-ui/core';
import styles from './TagType.module.scss'; import styles from './TagType.module.scss';
import commonStyles from '../common/common.module.scss'; import commonStyles from '../common/common.module.scss';
import AccessContext from '../../contexts/AccessContext'; 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'; 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 [tagTypeName, setTagTypeName] = useState(tagType.name || '');
const [tagTypeDescription, setTagTypeDescription] = useState(tagType.description || ''); const [tagTypeDescription, setTagTypeDescription] = useState(
tagType.description || ''
);
const [errors, setErrors] = useState({ const [errors, setErrors] = useState({
general: undefined, general: undefined,
name: undefined, name: undefined,
@ -53,11 +64,23 @@ const AddTagTypeComponent = ({ tagType, validateName, submit, history, editMode
const submitText = editMode ? 'Update' : 'Create'; const submitText = editMode ? 'Update' : 'Create';
return ( return (
<PageContent headerContent={`${submitText} Tag type`}> <PageContent headerContent={`${submitText} Tag type`}>
<section className={classnames(commonStyles.contentSpacing, styles.tagTypeContainer)}> <section
className={classnames(
commonStyles.contentSpacing,
styles.tagTypeContainer
)}
>
<Typography variant="subtitle1"> <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> </Typography>
<form onSubmit={onSubmit} className={classnames(styles.addTagTypeForm, commonStyles.contentSpacing)}> <form
onSubmit={onSubmit}
className={classnames(
styles.addTagTypeForm,
commonStyles.contentSpacing
)}
>
<TextField <TextField
label="Name" label="Name"
name="name" name="name"
@ -84,11 +107,22 @@ const AddTagTypeComponent = ({ tagType, validateName, submit, history, editMode
variant="outlined" variant="outlined"
size="small" size="small"
/> />
<ConditionallyRender condition={hasAccess(editMode ? UPDATE_TAG_TYPE : CREATE_TAG_TYPE)} show={ <ConditionallyRender
<div className={styles.formButtons}> condition={hasAccess(
<FormButtons submitText={submitText} onCancel={onCancel} /> editMode ? UPDATE_TAG_TYPE : CREATE_TAG_TYPE
</div> )}
} elseShow={<span>You do not have permissions to save.</span>} /> show={
<div className={styles.formButtons}>
<FormButtons
submitText={submitText}
onCancel={onCancel}
/>
</div>
}
elseShow={
<span>You do not have permissions to save.</span>
}
/>
</form> </form>
</section> </section>
</PageContent> </PageContent>

View File

@ -3,7 +3,16 @@ import PropTypes from 'prop-types';
import useMediaQuery from '@material-ui/core/useMediaQuery'; import useMediaQuery from '@material-ui/core/useMediaQuery';
import { useHistory } from 'react-router-dom'; 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 { CREATE_TAG, DELETE_TAG } from '../../AccessProvider/permissions';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import HeaderTitle from '../../common/HeaderTitle'; import HeaderTitle from '../../common/HeaderTitle';
@ -29,7 +38,10 @@ const TagList = ({ tags, fetchTags, removeTag }) => {
}; };
const listItem = tag => ( const listItem = tag => (
<ListItem key={`${tag.type}_${tag.value}`} className={styles.tagListItem}> <ListItem
key={`${tag.type}_${tag.value}`}
className={styles.tagListItem}
>
<ListItemIcon> <ListItemIcon>
<Icon>label</Icon> <Icon>label</Icon>
</ListItemIcon> </ListItemIcon>
@ -43,7 +55,9 @@ const TagList = ({ tags, fetchTags, removeTag }) => {
const DeleteButton = ({ tagType, tagValue }) => ( const DeleteButton = ({ tagType, tagValue }) => (
<Tooltip title="Delete tag"> <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> <Icon>delete</Icon>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@ -61,7 +75,10 @@ const TagList = ({ tags, fetchTags, removeTag }) => {
<ConditionallyRender <ConditionallyRender
condition={smallScreen} condition={smallScreen}
show={ show={
<IconButton aria-label="add tag" onClick={() => history.push('/tags/create')}> <IconButton
aria-label="add tag"
onClick={() => history.push('/tags/create')}
>
<Icon>add</Icon> <Icon>add</Icon>
</IconButton> </IconButton>
} }
@ -82,7 +99,14 @@ const TagList = ({ tags, fetchTags, removeTag }) => {
/> />
); );
return ( return (
<PageContent headerContent={<HeaderTitle title="Tags" actions={<AddButton hasAccess={hasAccess} />} />}> <PageContent
headerContent={
<HeaderTitle
title="Tags"
actions={<AddButton hasAccess={hasAccess} />}
/>
}
>
<List> <List>
<ConditionallyRender <ConditionallyRender
condition={tags.length > 0} condition={tags.length > 0}

View File

@ -4,12 +4,9 @@ import { Button, TextField } from '@material-ui/core';
import styles from './DemoAuth.module.scss'; import styles from './DemoAuth.module.scss';
const DemoAuth = ({ import logoIcon from '../../../assets/img/logo.png';
demoLogin,
loadInitialData, const DemoAuth = ({ demoLogin, history, authDetails }) => {
history,
authDetails,
}) => {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const handleSubmit = evt => { const handleSubmit = evt => {
@ -17,9 +14,7 @@ const DemoAuth = ({
const user = { email }; const user = { email };
const path = evt.target.action; const path = evt.target.action;
demoLogin(path, user) demoLogin(path, user).then(() => history.push(`/`));
.then(loadInitialData)
.then(() => history.push(`/`));
}; };
const handleChange = e => { const handleChange = e => {
@ -30,7 +25,7 @@ const DemoAuth = ({
return ( return (
<form onSubmit={handleSubmit} action={authDetails.path}> <form onSubmit={handleSubmit} action={authDetails.path}>
<div className={styles.container}> <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> <h2>Access the Unleash demo instance</h2>
<p>No further data or Credit Card required</p> <p>No further data or Credit Card required</p>
<div className={styles.form}> <div className={styles.form}>
@ -46,7 +41,6 @@ const DemoAuth = ({
type="email" type="email"
/> />
&nbsp;&nbsp; &nbsp;&nbsp;
<Button <Button
type="submit" type="submit"
variant="contained" variant="contained"
@ -58,16 +52,23 @@ const DemoAuth = ({
</Button> </Button>
</div> </div>
<p> <p>
By accessing our demo instance, you agree to the Unleash&nbsp; By accessing our demo instance, you agree to the
Unleash&nbsp;
<a <a
href="https://www.unleash-hosted.com/tos/" href="https://www.unleash-hosted.com/tos/"
target="_blank" rel="noreferrer"> target="_blank"
Customer Terms of Service rel="noreferrer"
</a> and&nbsp; >
<a href="https://www.unleash-hosted.com/privacy-policy/" target="_blank" rel="noreferrer"> Customer Terms of Service
</a>{' '}
and&nbsp;
<a
href="https://www.unleash-hosted.com/privacy-policy/"
target="_blank"
rel="noreferrer"
>
Privacy Policy Privacy Policy
</a> </a>
</p> </p>
</div> </div>
</form> </form>
@ -77,7 +78,6 @@ const DemoAuth = ({
DemoAuth.propTypes = { DemoAuth.propTypes = {
authDetails: PropTypes.object.isRequired, authDetails: PropTypes.object.isRequired,
demoLogin: PropTypes.func.isRequired, demoLogin: PropTypes.func.isRequired,
loadInitialData: PropTypes.func.isRequired,
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired,
}; };

View File

@ -4,6 +4,7 @@ import classnames from 'classnames';
import { SyntheticEvent, useState } from 'react'; import { SyntheticEvent, useState } from 'react';
import { useCommonStyles } from '../../../common.styles'; import { useCommonStyles } from '../../../common.styles';
import useLoading from '../../../hooks/useLoading'; import useLoading from '../../../hooks/useLoading';
import { formatApiPath } from '../../../utils/format-path';
import ConditionallyRender from '../../common/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender';
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout'; import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
import { useStyles } from './ForgottenPassword.styles'; import { useStyles } from './ForgottenPassword.styles';
@ -22,7 +23,8 @@ const ForgottenPassword = () => {
setLoading(true); setLoading(true);
setAttemptedEmail(email); setAttemptedEmail(email);
await fetch('auth/reset/password-email', { const path = formatApiPath('auth/reset/password-email');
await fetch(path, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },

View File

@ -8,17 +8,10 @@ import useQueryParams from '../../../hooks/useQueryParams';
import ResetPasswordSuccess from '../common/ResetPasswordSuccess/ResetPasswordSuccess'; import ResetPasswordSuccess from '../common/ResetPasswordSuccess/ResetPasswordSuccess';
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout'; import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
const Login = ({ history, loadInitialData, isUnauthorized, authDetails }) => { const Login = ({ history, isUnauthorized, authDetails }) => {
const styles = useStyles(); const styles = useStyles();
const query = useQueryParams(); const query = useQueryParams();
useEffect(() => {
if (isUnauthorized()) {
loadInitialData();
}
/* eslint-disable-next-line */
}, []);
useEffect(() => { useEffect(() => {
if (!isUnauthorized()) { if (!isUnauthorized()) {
history.push('features'); history.push('features');

View File

@ -1,14 +1,9 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Login from './Login'; import Login from './Login';
import { loadInitialData } from './../../../store/loader';
const mapDispatchToProps = (dispatch, props) => ({
loadInitialData: () => loadInitialData(props.flags)(dispatch),
});
const mapStateToProps = state => ({ const mapStateToProps = state => ({
user: state.user.toJS(), user: state.user.toJS(),
flags: state.uiConfig.toJS().flags, flags: state.uiConfig.toJS().flags,
}); });
export default connect(mapStateToProps, mapDispatchToProps)(Login); export default connect(mapStateToProps)(Login);

View File

@ -8,7 +8,7 @@ import { useCommonStyles } from '../../../common.styles';
import { useStyles } from './PasswordAuth.styles'; import { useStyles } from './PasswordAuth.styles';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
const PasswordAuth = ({ authDetails, passwordLogin, loadInitialData }) => { const PasswordAuth = ({ authDetails, passwordLogin }) => {
const commonStyles = useCommonStyles(); const commonStyles = useCommonStyles();
const styles = useStyles(); const styles = useStyles();
const history = useHistory(); const history = useHistory();
@ -50,7 +50,7 @@ const PasswordAuth = ({ authDetails, passwordLogin, loadInitialData }) => {
try { try {
await passwordLogin(path, user); await passwordLogin(path, user);
await loadInitialData();
history.push(`/`); history.push(`/`);
} catch (error) { } catch (error) {
if (error.statusCode === 404 || error.statusCode === 400) { if (error.statusCode === 404 || error.statusCode === 400) {
@ -168,7 +168,6 @@ const PasswordAuth = ({ authDetails, passwordLogin, loadInitialData }) => {
PasswordAuth.propTypes = { PasswordAuth.propTypes = {
authDetails: PropTypes.object.isRequired, authDetails: PropTypes.object.isRequired,
passwordLogin: PropTypes.func.isRequired, passwordLogin: PropTypes.func.isRequired,
loadInitialData: PropTypes.func.isRequired,
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired,
}; };

View File

@ -4,12 +4,7 @@ import { Button, TextField } from '@material-ui/core';
import styles from './SimpleAuth.module.scss'; import styles from './SimpleAuth.module.scss';
const SimpleAuth = ({ const SimpleAuth = ({ insecureLogin, history, authDetails }) => {
insecureLogin,
loadInitialData,
history,
authDetails,
}) => {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const handleSubmit = evt => { const handleSubmit = evt => {
@ -17,9 +12,7 @@ const SimpleAuth = ({
const user = { email }; const user = { email };
const path = evt.target.action; const path = evt.target.action;
insecureLogin(path, user) insecureLogin(path, user).then(() => history.push(`/`));
.then(loadInitialData)
.then(() => history.push(`/`));
}; };
const handleChange = e => { const handleChange = e => {
@ -74,7 +67,6 @@ const SimpleAuth = ({
SimpleAuth.propTypes = { SimpleAuth.propTypes = {
authDetails: PropTypes.object.isRequired, authDetails: PropTypes.object.isRequired,
insecureLogin: PropTypes.func.isRequired, insecureLogin: PropTypes.func.isRequired,
loadInitialData: PropTypes.func.isRequired,
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired,
}; };

View File

@ -2,9 +2,9 @@ import { FC } from 'react';
import { Typography, useTheme } from '@material-ui/core'; import { Typography, useTheme } from '@material-ui/core';
import Gradient from '../../common/Gradient/Gradient'; import Gradient from '../../common/Gradient/Gradient';
import { ReactComponent as StarIcon } from '../../../icons/star.svg'; import { ReactComponent as StarIcon } from '../../../assets/icons/star.svg';
import { ReactComponent as RightToggleIcon } from '../../../icons/toggleRight.svg'; import { ReactComponent as RightToggleIcon } from '../../../assets/icons/toggleRight.svg';
import { ReactComponent as LeftToggleIcon } from '../../../icons/toggleLeft.svg'; import { ReactComponent as LeftToggleIcon } from '../../../assets/icons/toggleLeft.svg';
import { useStyles } from './StandaloneBanner.styles'; import { useStyles } from './StandaloneBanner.styles';
import ConditionallyRender from '../../common/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender';

View File

@ -15,6 +15,7 @@ import {
OK, OK,
UNAUTHORIZED, UNAUTHORIZED,
} from '../../../../constants/statusCodes'; } from '../../../../constants/statusCodes';
import { formatApiPath } from '../../../../utils/format-path';
interface IEditProfileProps { interface IEditProfileProps {
setEditingProfile: React.Dispatch<React.SetStateAction<boolean>>; setEditingProfile: React.Dispatch<React.SetStateAction<boolean>>;
@ -45,7 +46,8 @@ const EditProfile = ({
setLoading(true); setLoading(true);
setError(''); setError('');
try { try {
const res = await fetch('api/admin/user/change-password', { const path = formatApiPath('api/admin/user/change-password');
const res = await fetch(path, {
headers, headers,
body: JSON.stringify({ password, confirmPassword }), body: JSON.stringify({ password, confirmPassword }),
method: 'POST', method: 'POST',

View File

@ -37,7 +37,6 @@ const UserProfile = ({
useEffect(() => { useEffect(() => {
fetchUser(); fetchUser();
const locale = navigator.language || navigator.userLanguage; const locale = navigator.language || navigator.userLanguage;
let found = possibleLocales.find(l => let found = possibleLocales.find(l =>
l.toLowerCase().includes(locale.toLowerCase()) l.toLowerCase().includes(locale.toLowerCase())

View File

@ -24,7 +24,6 @@ const UserProfileContent = ({
imageUrl, imageUrl,
currentLocale, currentLocale,
setCurrentLocale, setCurrentLocale,
location,
logoutUser, logoutUser,
}) => { }) => {
const commonStyles = useCommonStyles(); const commonStyles = useCommonStyles();

View File

@ -17,7 +17,6 @@ class AuthComponent extends React.Component {
demoLogin: PropTypes.func.isRequired, demoLogin: PropTypes.func.isRequired,
insecureLogin: PropTypes.func.isRequired, insecureLogin: PropTypes.func.isRequired,
passwordLogin: PropTypes.func.isRequired, passwordLogin: PropTypes.func.isRequired,
loadInitialData: PropTypes.func.isRequired,
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired,
}; };
@ -31,7 +30,6 @@ class AuthComponent extends React.Component {
<PasswordAuth <PasswordAuth
passwordLogin={this.props.passwordLogin} passwordLogin={this.props.passwordLogin}
authDetails={authDetails} authDetails={authDetails}
loadInitialData={this.props.loadInitialData}
history={this.props.history} history={this.props.history}
/> />
); );
@ -40,7 +38,6 @@ class AuthComponent extends React.Component {
<SimpleAuth <SimpleAuth
insecureLogin={this.props.insecureLogin} insecureLogin={this.props.insecureLogin}
authDetails={authDetails} authDetails={authDetails}
loadInitialData={this.props.loadInitialData}
history={this.props.history} history={this.props.history}
/> />
); );
@ -49,7 +46,6 @@ class AuthComponent extends React.Component {
<DemoAuth <DemoAuth
demoLogin={this.props.demoLogin} demoLogin={this.props.demoLogin}
authDetails={authDetails} authDetails={authDetails}
loadInitialData={this.props.loadInitialData}
history={this.props.history} history={this.props.history}
/> />
); );

View File

@ -1,13 +1,15 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import AuthenticationComponent from './authentication-component'; import AuthenticationComponent from './authentication-component';
import { insecureLogin, passwordLogin, demoLogin } from '../../store/user/actions'; import {
import { loadInitialData } from './../../store/loader'; insecureLogin,
passwordLogin,
demoLogin,
} from '../../store/user/actions';
const mapDispatchToProps = (dispatch, props) => ({ const mapDispatchToProps = (dispatch, props) => ({
demoLogin: (path, user) => demoLogin(path, user)(dispatch), demoLogin: (path, user) => demoLogin(path, user)(dispatch),
insecureLogin: (path, user) => insecureLogin(path, user)(dispatch), insecureLogin: (path, user) => insecureLogin(path, user)(dispatch),
passwordLogin: (path, user) => passwordLogin(path, user)(dispatch), passwordLogin: (path, user) => passwordLogin(path, user)(dispatch),
loadInitialData: () => loadInitialData(props.flags)(dispatch),
}); });
const mapStateToProps = state => ({ const mapStateToProps = state => ({
@ -15,4 +17,7 @@ const mapStateToProps = state => ({
flags: state.uiConfig.toJS().flags, flags: state.uiConfig.toJS().flags,
}); });
export default connect(mapStateToProps, mapDispatchToProps)(AuthenticationComponent); export default connect(
mapStateToProps,
mapDispatchToProps
)(AuthenticationComponent);

View File

@ -5,6 +5,7 @@ import { BAD_REQUEST, OK } from '../../../../../constants/statusCodes';
import { useStyles } from './PasswordChecker.styles'; import { useStyles } from './PasswordChecker.styles';
import HelpIcon from '@material-ui/icons/Help'; import HelpIcon from '@material-ui/icons/Help';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { formatApiPath } from '../../../../../utils/format-path';
interface IPasswordCheckerProps { interface IPasswordCheckerProps {
password: string; password: string;
@ -37,7 +38,8 @@ const PasswordChecker = ({ password, callback }: IPasswordCheckerProps) => {
const [lengthError, setLengthError] = useState(true); const [lengthError, setLengthError] = useState(true);
const makeValidatePassReq = useCallback(() => { const makeValidatePassReq = useCallback(() => {
return fetch('auth/reset/validate-password', { const path = formatApiPath('auth/reset/validate-password');
return fetch(path, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -63,7 +65,7 @@ const PasswordChecker = ({ password, callback }: IPasswordCheckerProps) => {
} }
} catch (e) { } catch (e) {
// ResetPasswordForm handles errors related to submitting the form. // ResetPasswordForm handles errors related to submitting the form.
console.log(e); console.log('An exception was caught and handled');
} }
}, [makeValidatePassReq, callback, password]); }, [makeValidatePassReq, callback, password]);

View File

@ -16,6 +16,7 @@ import PasswordChecker from './PasswordChecker/PasswordChecker';
import PasswordMatcher from './PasswordMatcher/PasswordMatcher'; import PasswordMatcher from './PasswordMatcher/PasswordMatcher';
import { useStyles } from './ResetPasswordForm.styles'; import { useStyles } from './ResetPasswordForm.styles';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
interface IResetPasswordProps { interface IResetPasswordProps {
token: string; token: string;
@ -47,7 +48,8 @@ const ResetPasswordForm = ({ token, setLoading }: IResetPasswordProps) => {
}, [password, confirmPassword]); }, [password, confirmPassword]);
const makeResetPasswordReq = () => { const makeResetPasswordReq = () => {
return fetch('auth/reset/password', { const path = formatApiPath('auth/reset/password');
return fetch(path, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },

View File

@ -13,6 +13,7 @@ import {
headers, headers,
NotFoundError, NotFoundError,
} from '../store/api-helper'; } from '../store/api-helper';
import { formatApiPath } from '../utils/format-path';
export interface IUserApiErrors { export interface IUserApiErrors {
addUser?: string; addUser?: string;
@ -62,7 +63,8 @@ const useAdminUsersApi = () => {
const addUser = async (user: IUserPayload) => { const addUser = async (user: IUserPayload) => {
return makeRequest(() => { return makeRequest(() => {
return fetch('api/admin/user-admin', { const path = formatApiPath('api/admin/user-admin');
return fetch(path, {
...defaultOptions, ...defaultOptions,
method: 'POST', method: 'POST',
body: JSON.stringify(user), body: JSON.stringify(user),
@ -72,7 +74,8 @@ const useAdminUsersApi = () => {
const removeUser = async (user: IUserPayload) => { const removeUser = async (user: IUserPayload) => {
return makeRequest(() => { return makeRequest(() => {
return fetch(`api/admin/user-admin/${user.id}`, { const path = formatApiPath(`api/admin/user-admin/${user.id}`);
return fetch(path, {
...defaultOptions, ...defaultOptions,
method: 'DELETE', method: 'DELETE',
}); });
@ -81,7 +84,9 @@ const useAdminUsersApi = () => {
const updateUser = async (user: IUserPayload) => { const updateUser = async (user: IUserPayload) => {
return makeRequest(() => { return makeRequest(() => {
return fetch(`api/admin/user-admin/${user.id}`, { const path = formatApiPath(`api/admin/user-admin/${user.id}`);
return fetch(path, {
...defaultOptions, ...defaultOptions,
method: 'PUT', method: 'PUT',
body: JSON.stringify(user), body: JSON.stringify(user),
@ -91,7 +96,10 @@ const useAdminUsersApi = () => {
const changePassword = async (user: IUserPayload, password: string) => { const changePassword = async (user: IUserPayload, password: string) => {
return makeRequest(() => { 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, ...defaultOptions,
method: 'POST', method: 'POST',
body: JSON.stringify({ password }), body: JSON.stringify({ password }),
@ -101,7 +109,10 @@ const useAdminUsersApi = () => {
const validatePassword = async (password: string) => { const validatePassword = async (password: string) => {
return makeRequest(() => { return makeRequest(() => {
return fetch(`api/admin/user-admin/validate-password`, { const path = formatApiPath(
`api/admin/user-admin/validate-password`
);
return fetch(path, {
...defaultOptions, ...defaultOptions,
method: 'POST', method: 'POST',
body: JSON.stringify({ password }), body: JSON.stringify({ password }),

View File

@ -1,11 +1,14 @@
import useSWR from 'swr'; import useSWR from 'swr';
import useQueryParams from './useQueryParams'; import useQueryParams from './useQueryParams';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from '../utils/format-path';
const getFetcher = (token: string) => () => const getFetcher = (token: string) => () => {
fetch(`auth/reset/validate?token=${token}`, { const path = formatApiPath(`auth/reset/validate?token=${token}`);
return fetch(path, {
method: 'GET', method: 'GET',
}).then(res => res.json()); }).then(res => res.json());
};
const INVALID_TOKEN_ERROR = 'InvalidTokenError'; const INVALID_TOKEN_ERROR = 'InvalidTokenError';
const USED_TOKEN_ERROR = 'UsedTokenError'; const USED_TOKEN_ERROR = 'UsedTokenError';
@ -16,10 +19,9 @@ const useResetPassword = () => {
const [token, setToken] = useState(initialToken); const [token, setToken] = useState(initialToken);
const fetcher = getFetcher(token); const fetcher = getFetcher(token);
const { data, error } = useSWR(
`auth/reset/validate?token=${token}`, const key = `auth/reset/validate?token=${token}`;
fetcher const { data, error } = useSWR(key, fetcher);
);
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
const retry = () => { const retry = () => {

View File

@ -1,11 +1,14 @@
import useSWR, { mutate } from 'swr'; import useSWR, { mutate } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from '../utils/format-path';
const useUsers = () => { const useUsers = () => {
const fetcher = () => const fetcher = () => {
fetch(`api/admin/user-admin`, { const path = formatApiPath(`api/admin/user-admin`);
return fetch(path, {
method: 'GET', method: 'GET',
}).then(res => res.json()); }).then(res => res.json());
};
const { data, error } = useSWR(`api/admin/user-admin`, fetcher); const { data, error } = useSWR(`api/admin/user-admin`, fetcher);
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);

View File

@ -3,7 +3,7 @@ import 'whatwg-fetch';
import './app.css'; import './app.css';
import ReactDOM from 'react-dom'; 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 { Provider } from 'react-redux';
import { ThemeProvider, CssBaseline } from '@material-ui/core'; import { ThemeProvider, CssBaseline } from '@material-ui/core';
import thunkMiddleware from 'redux-thunk'; import thunkMiddleware from 'redux-thunk';
@ -15,10 +15,11 @@ import { StylesProvider } from '@material-ui/core/styles';
import mainTheme from './themes/main-theme'; import mainTheme from './themes/main-theme';
import store from './store'; import store from './store';
import MetricsPoller from './metrics-poller'; import MetricsPoller from './metrics-poller';
import App from './component/App'; import App from './component/AppContainer';
import ScrollToTop from './component/scroll-to-top'; import ScrollToTop from './component/scroll-to-top';
import { writeWarning } from './security-logger'; import { writeWarning } from './security-logger';
import AccessProvider from './component/AccessProvider/AccessProvider'; import AccessProvider from './component/AccessProvider/AccessProvider';
import { getBasePath } from './utils/format-path';
let composeEnhancers; let composeEnhancers;
@ -43,7 +44,7 @@ ReactDOM.render(
<Provider store={unleashStore}> <Provider store={unleashStore}>
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<AccessProvider store={unleashStore}> <AccessProvider store={unleashStore}>
<HashRouter> <Router basename={`${getBasePath()}`}>
<ThemeProvider theme={mainTheme}> <ThemeProvider theme={mainTheme}>
<StylesProvider injectFirst> <StylesProvider injectFirst>
<CssBaseline /> <CssBaseline />
@ -52,7 +53,7 @@ ReactDOM.render(
</ScrollToTop> </ScrollToTop>
</StylesProvider> </StylesProvider>
</ThemeProvider> </ThemeProvider>
</HashRouter> </Router>
</AccessProvider> </AccessProvider>
</DndProvider> </DndProvider>
</Provider>, </Provider>,

View File

@ -1,7 +1,15 @@
/* eslint-disable no-alert */ /* eslint-disable no-alert */
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types'; 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 { Alert } from '@material-ui/lab';
import { formatFullDateTimeWithLocale } from '../../../component/common/util'; import { formatFullDateTimeWithLocale } from '../../../component/common/util';
import CreateApiKey from './api-key-create'; import CreateApiKey from './api-key-create';
@ -9,9 +17,19 @@ import Secret from './secret';
import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender';
import Dialogue from '../../../component/common/Dialogue/Dialogue'; import Dialogue from '../../../component/common/Dialogue/Dialogue';
import AccessContext from '../../../contexts/AccessContext'; 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 { hasAccess } = useContext(AccessContext);
const [showDelete, setShowDelete] = useState(false); const [showDelete, setShowDelete] = useState(false);
const [delKey, setDelKey] = useState(undefined); const [delKey, setDelKey] = useState(undefined);
@ -28,20 +46,27 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, unleashUr
return ( return (
<div> <div>
<Alert severity="info" > <Alert severity="info">
<p> <p>
Read the{' '} 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 Getting started guide
</a>{' '} </a>{' '}
to learn how to connect to the Unleash API form your application or programmatically. to learn how to connect to the Unleash API from your
Please note it can take up to 1 minute before a new API key is activated. application or programmatically. Please note it can take up
to 1 minute before a new API key is activated.
</p> </p>
<br /> <br />
<strong>API URL: </strong> <pre style={{display: 'inline'}}>{unleashUrl}/api/</pre> <strong>API URL: </strong>{' '}
<pre style={{ display: 'inline' }}>{unleashUrl}/api/</pre>
</Alert> </Alert>
<br /><br /> <br />
<br />
<br /> <br />
<Table> <Table>
@ -58,10 +83,17 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, unleashUr
{keys.map(item => ( {keys.map(item => (
<TableRow key={item.secret}> <TableRow key={item.secret}>
<TableCell style={{ textAlign: 'left' }}> <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>
<TableCell style={{ textAlign: 'left' }}>{item.username}</TableCell>
<TableCell style={{ textAlign: 'left' }}>{item.type}</TableCell>
<TableCell style={{ textAlign: 'left' }}> <TableCell style={{ textAlign: 'left' }}>
<Secret value={item.secret} /> <Secret value={item.secret} />
</TableCell> </TableCell>
@ -95,7 +127,10 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, unleashUr
> >
<div>Are you sure you want to delete?</div> <div>Are you sure you want to delete?</div>
</Dialogue> </Dialogue>
<ConditionallyRender condition={hasAccess(CREATE_API_TOKEN)} show={<CreateApiKey addKey={addKey} />} /> <ConditionallyRender
condition={hasAccess(CREATE_API_TOKEN)}
show={<CreateApiKey addKey={addKey} />}
/>
</div> </div>
); );
} }

View File

@ -12,7 +12,12 @@ const initialState = {
unleashHostname: location.hostname, unleashHostname: location.hostname,
}; };
function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl }) { function GoogleAuth({
config,
getGoogleConfig,
updateGoogleConfig,
unleashUrl,
}) {
const [data, setData] = useState(initialState); const [data, setData] = useState(initialState);
const [info, setInfo] = useState(); const [info, setInfo] = useState();
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
@ -64,11 +69,16 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
<Grid item xs={12}> <Grid item xs={12}>
<Alert severity="info"> <Alert severity="info">
Please read the{' '} 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 documentation
</a>{' '} </a>{' '}
to learn how to integrate with Google OAuth 2.0. <br /> 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> </Alert>
</Grid> </Grid>
</Grid> </Grid>
@ -77,12 +87,16 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
<Grid item xs={5}> <Grid item xs={5}>
<strong>Enable</strong> <strong>Enable</strong>
<p> <p>
Enable Google users to login. Value is ignored if Client ID and Client Secret are not Enable Google users to login. Value is ignored if
defined. Client ID and Client Secret are not defined.
</p> </p>
</Grid> </Grid>
<Grid item xs={6} style={{ padding: '20px' }}> <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'} {data.enabled ? 'Enabled' : 'Disabled'}
</Switch> </Switch>
</Grid> </Grid>
@ -90,7 +104,10 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={5}> <Grid item xs={5}>
<strong>Client ID</strong> <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>
<Grid item xs={6}> <Grid item xs={6}>
<TextField <TextField
@ -108,7 +125,10 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item md={5}> <Grid item md={5}>
<strong>Client Secret</strong> <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>
<Grid item md={6}> <Grid item md={6}>
<TextField <TextField
@ -127,10 +147,13 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
<Grid item md={5}> <Grid item md={5}>
<strong>Unleash hostname</strong> <strong>Unleash hostname</strong>
<p> <p>
(Required) The hostname you are running Unleash on that Google should send the user back to. (Required) The hostname you are running Unleash on
The final callback URL will be{' '} that Google should send the user back to. The final
callback URL will be{' '}
<small> <small>
<code>https://[unleash.hostname.com]/auth/google/callback</code> <code>
https://[unleash.hostname.com]/auth/google/callback
</code>
</small> </small>
</p> </p>
</Grid> </Grid>
@ -150,10 +173,17 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item md={5}> <Grid item md={5}>
<strong>Auto-create users</strong> <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>
<Grid item md={6} style={{ padding: '20px' }}> <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 Auto-create users
</Switch> </Switch>
</Grid> </Grid>
@ -161,7 +191,10 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item md={5}> <Grid item md={5}>
<strong>Email domains</strong> <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>
<Grid item md={6}> <Grid item md={6}>
<TextField <TextField
@ -180,7 +213,11 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
</Grid> </Grid>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item md={5}> <Grid item md={5}>
<Button variant="contained" color="primary" type="submit"> <Button
variant="contained"
color="primary"
type="submit"
>
Save Save
</Button>{' '} </Button>{' '}
<small>{info}</small> <small>{info}</small>

View File

@ -4,7 +4,7 @@ import { Button, Grid, Switch, TextField } from '@material-ui/core';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
import PageContent from '../../../component/common/PageContent/PageContent'; import PageContent from '../../../component/common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext'; import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../../component/AccessProvider/permissions'; import { ADMIN } from '../../../component/AccessProvider/permissions';
const initialState = { const initialState = {
enabled: false, enabled: false,
@ -30,7 +30,11 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
}, [config]); }, [config]);
if (!hasAccess(ADMIN)) { 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 => { const updateField = e => {
@ -65,11 +69,17 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
<Grid item md={12}> <Grid item md={12}>
<Alert severity="info"> <Alert severity="info">
Please read the{' '} 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 documentation
</a>{' '} </a>{' '}
to learn how to integrate with specific SAML 2.0 providers (Okta, Keycloak, etc). <br /> to learn how to integrate with specific SAML 2.0
Callback URL: <code>{unleashUrl}/auth/saml/callback</code> providers (Okta, Keycloak, etc). <br />
Callback URL:{' '}
<code>{unleashUrl}/auth/saml/callback</code>
</Alert> </Alert>
</Grid> </Grid>
</Grid> </Grid>
@ -80,7 +90,12 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
<p>Enable SAML 2.0 Authentication.</p> <p>Enable SAML 2.0 Authentication.</p>
</Grid> </Grid>
<Grid item md={6}> <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'} {data.enabled ? 'Enabled' : 'Disabled'}
</Switch> </Switch>
</Grid> </Grid>
@ -105,7 +120,10 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item md={5}> <Grid item md={5}>
<strong>Single Sign-On URL</strong> <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>
<Grid item md={6}> <Grid item md={6}>
<TextField <TextField
@ -122,7 +140,10 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item md={5}> <Grid item md={5}>
<strong>X.509 Certificate</strong> <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>
<Grid item md={7}> <Grid item md={7}>
<TextField <TextField
@ -146,10 +167,17 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item md={5}> <Grid item md={5}>
<strong>Auto-create users</strong> <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>
<Grid item md={6} style={{ padding: '20px' }}> <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 Auto-create users
</Switch> </Switch>
</Grid> </Grid>
@ -157,7 +185,10 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item md={5}> <Grid item md={5}>
<strong>Email domains</strong> <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>
<Grid item md={6}> <Grid item md={6}>
<TextField <TextField
@ -175,7 +206,11 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
</Grid> </Grid>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item md={5}> <Grid item md={5}>
<Button variant="contained" color="primary" type="submit"> <Button
variant="contained"
color="primary"
type="submit"
>
Save Save
</Button>{' '} </Button>{' '}
<small>{info}</small> <small>{info}</small>

View File

@ -1,7 +1,7 @@
import { Typography } from '@material-ui/core'; import { Typography } from '@material-ui/core';
import Dialogue from '../../../../../component/common/Dialogue'; 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'; import { useStyles } from './ConfirmUserEmail.styles';
interface IConfirmUserEmailProps { interface IConfirmUserEmailProps {

View File

@ -8,7 +8,7 @@ import ConditionallyRender from '../../../component/common/ConditionallyRender';
import { ADMIN } from '../../../component/AccessProvider/permissions'; import { ADMIN } from '../../../component/AccessProvider/permissions';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
const UsersAdmin = ({history}) => { const UsersAdmin = ({ history }) => {
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
return ( return (
@ -16,14 +16,18 @@ const UsersAdmin = ({history}) => {
<AdminMenu history={history} /> <AdminMenu history={history} />
<PageContent headerContent="Users"> <PageContent headerContent="Users">
<ConditionallyRender <ConditionallyRender
condition={hasAccess(ADMIN)} condition={hasAccess(ADMIN)}
show={<UsersList />} show={<UsersList />}
elseShow={<Alert severity="error">You need to be a root admin to access this section.</Alert>} /> elseShow={
<Alert severity="error">
You need to be a root admin to access this section.
</Alert>
}
/>
</PageContent> </PageContent>
</div> </div>
); );
} };
UsersAdmin.propTypes = { UsersAdmin.propTypes = {
match: PropTypes.object.isRequired, match: PropTypes.object.isRequired,

View File

@ -1,6 +1,7 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess, headers } from '../api-helper'; import { throwIfNotSuccess, headers } from '../api-helper';
const URI = 'api/admin/addons'; const URI = formatApiPath(`api/admin/addons`);
function fetchAll() { function fetchAll() {
return fetch(URI, { credentials: 'include' }) return fetch(URI, { credentials: 'include' })

View File

@ -1,6 +1,7 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess, headers } from '../api-helper'; import { throwIfNotSuccess, headers } from '../api-helper';
const URI = 'api/admin/metrics/applications'; const URI = formatApiPath('api/admin/metrics/applications');
function fetchAll() { function fetchAll() {
return fetch(URI, { headers, credentials: 'include' }) return fetch(URI, { headers, credentials: 'include' })

View File

@ -1,6 +1,7 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess, headers } from '../api-helper'; import { throwIfNotSuccess, headers } from '../api-helper';
const URI = 'api/admin/archive'; const URI = formatApiPath('api/admin/archive');
function fetchAll() { function fetchAll() {
return fetch(`${URI}/features`, { credentials: 'include' }) return fetch(`${URI}/features`, { credentials: 'include' })

View File

@ -10,7 +10,7 @@ export const ERROR_ADD_CONTEXT_FIELD = 'ERROR_ADD_CONTEXT_FIELD';
export const UPDATE_CONTEXT_FIELD = 'UPDATE_CONTEXT_FIELD'; export const UPDATE_CONTEXT_FIELD = 'UPDATE_CONTEXT_FIELD';
export const ERROR_UPDATE_CONTEXT_FIELD = 'ERROR_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 addContextField = context => ({ type: ADD_CONTEXT_FIELD, context });
const upContextField = context => ({ type: UPDATE_CONTEXT_FIELD, context }); const upContextField = context => ({ type: UPDATE_CONTEXT_FIELD, context });
const createRemoveContext = context => ({ type: REMOVE_CONTEXT, context }); const createRemoveContext = context => ({ type: REMOVE_CONTEXT, context });

View File

@ -1,6 +1,7 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess, headers } from '../api-helper'; import { throwIfNotSuccess, headers } from '../api-helper';
const URI = 'api/admin/context'; const URI = formatApiPath('api/admin/context');
function fetchAll() { function fetchAll() {
return fetch(URI, { credentials: 'include' }) return fetch(URI, { credentials: 'include' })

View File

@ -1,5 +1,10 @@
import { List } from 'immutable'; 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'; import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
const DEFAULT_CONTEXT_FIELDS = [ const DEFAULT_CONTEXT_FIELDS = [
@ -21,7 +26,9 @@ const strategies = (state = getInitState(), action) => {
case ADD_CONTEXT_FIELD: case ADD_CONTEXT_FIELD:
return state.push(action.context); return state.push(action.context);
case UPDATE_CONTEXT_FIELD: { 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); return state.set(index, action.context);
} }
case USER_LOGOUT: case USER_LOGOUT:

View File

@ -1,6 +1,7 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess, headers } from '../api-helper'; import { throwIfNotSuccess, headers } from '../api-helper';
const URI = 'api/admin/api-tokens'; const URI = formatApiPath('api/admin/api-tokens');
function fetchAll() { function fetchAll() {
return fetch(URI, { headers, credentials: 'include' }) return fetch(URI, { headers, credentials: 'include' })

View File

@ -1,6 +1,7 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess } from '../api-helper'; import { throwIfNotSuccess } from '../api-helper';
const URI = 'api/admin/metrics/feature-toggles'; const URI = formatApiPath('api/admin/metrics/feature-toggles');
function fetchFeatureMetrics() { function fetchFeatureMetrics() {
return fetch(URI, { credentials: 'include' }) return fetch(URI, { credentials: 'include' })
@ -8,7 +9,7 @@ function fetchFeatureMetrics() {
.then(response => response.json()); .then(response => response.json());
} }
const seenURI = 'api/admin/metrics/seen-apps'; const seenURI = formatApiPath('api/admin/metrics/seen-apps');
function fetchSeenApps() { function fetchSeenApps() {
return fetch(seenURI, { credentials: 'include' }) return fetch(seenURI, { credentials: 'include' })

View File

@ -1,6 +1,7 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess, headers } from '../api-helper'; import { throwIfNotSuccess, headers } from '../api-helper';
const URI = 'api/admin/features'; const URI = formatApiPath('api/admin/features');
function tagFeature(featureToggle, tag) { function tagFeature(featureToggle, tag) {
return fetch(`${URI}/${featureToggle}/tags`, { return fetch(`${URI}/${featureToggle}/tags`, {
@ -14,11 +15,16 @@ function tagFeature(featureToggle, tag) {
} }
function untagFeature(featureToggle, tag) { function untagFeature(featureToggle, tag) {
return fetch(`${URI}/${featureToggle}/tags/${tag.type}/${encodeURIComponent(tag.value)}`, { return fetch(
method: 'DELETE', `${URI}/${featureToggle}/tags/${tag.type}/${encodeURIComponent(
headers, tag.value
credentials: 'include', )}`,
}).then(throwIfNotSuccess); {
method: 'DELETE',
headers,
credentials: 'include',
}
).then(throwIfNotSuccess);
} }
function fetchFeatureToggleTags(featureToggle) { function fetchFeatureToggleTags(featureToggle) {

View File

@ -1,10 +1,14 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess, headers } from '../api-helper'; import { throwIfNotSuccess, headers } from '../api-helper';
const URI = 'api/admin/features'; const URI = formatApiPath('api/admin/features');
function validateToggle(featureToggle) { function validateToggle(featureToggle) {
return new Promise((resolve, reject) => { 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')); reject(new Error('You must add at least one activation strategy'));
} else { } else {
resolve(featureToggle); resolve(featureToggle);

View File

@ -4,12 +4,15 @@ import { dispatchError } from '../util';
export const RECEIVE_FEATURE_TYPES = 'RECEIVE_FEATURE_TYPES'; export const RECEIVE_FEATURE_TYPES = 'RECEIVE_FEATURE_TYPES';
export const ERROR_RECEIVE_FEATURE_TYPES = 'ERROR_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() { export function fetchFeatureTypes() {
return dispatch => return dispatch =>
api api
.fetchAll() .fetchAll()
.then(json => dispatch(receiveFeatureTypes(json))) .then(json => dispatch(receiveFeatureTypes(json.types)))
.catch(dispatchError(dispatch, ERROR_RECEIVE_FEATURE_TYPES)); .catch(dispatchError(dispatch, ERROR_RECEIVE_FEATURE_TYPES));
} }

View File

@ -1,6 +1,7 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess } from '../api-helper'; import { throwIfNotSuccess } from '../api-helper';
const URI = 'api/admin/feature-types'; const URI = formatApiPath('api/admin/feature-types');
function fetchAll() { function fetchAll() {
return fetch(URI, { credentials: 'include' }) return fetch(URI, { credentials: 'include' })

View File

@ -1,7 +1,9 @@
import { List } from 'immutable'; import { List } from 'immutable';
import { RECEIVE_FEATURE_TYPES } from './actions'; 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() { function getInitState() {
return new List(DEFAULT_FEATURE_TYPES); return new List(DEFAULT_FEATURE_TYPES);
@ -10,7 +12,7 @@ function getInitState() {
const strategies = (state = getInitState(), action) => { const strategies = (state = getInitState(), action) => {
switch (action.type) { switch (action.type) {
case RECEIVE_FEATURE_TYPES: case RECEIVE_FEATURE_TYPES:
return new List(action.value.types); return new List(action.value);
default: default:
return state; return state;
} }

View File

@ -1,6 +1,7 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess } from '../api-helper'; import { throwIfNotSuccess } from '../api-helper';
const URI = 'api/admin/events'; const URI = formatApiPath('api/admin/events');
function fetchAll() { function fetchAll() {
return fetch(URI, { credentials: 'include' }) return fetch(URI, { credentials: 'include' })

View File

@ -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);
};
}

View File

@ -13,9 +13,9 @@ export const ERROR_UPDATE_PROJECT = 'ERROR_UPDATE_PROJECT';
const addProject = project => ({ type: ADD_PROJECT, project }); const addProject = project => ({ type: ADD_PROJECT, project });
const upProject = project => ({ type: UPDATE_PROJECT, project }); const upProject = project => ({ type: UPDATE_PROJECT, project });
const delProject = project => ({ type: REMOVE_PROJECT, project }); const delProject = project => ({ type: REMOVE_PROJECT, project });
export const receiveProjects = value => ({ type: RECEIVE_PROJECT, value });
export function fetchProjects() { export function fetchProjects() {
const receiveProjects = value => ({ type: RECEIVE_PROJECT, value });
return dispatch => return dispatch =>
api api
.fetchAll() .fetchAll()

View File

@ -1,6 +1,7 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess, headers } from '../api-helper'; import { throwIfNotSuccess, headers } from '../api-helper';
const URI = 'api/admin/projects'; const URI = formatApiPath('api/admin/projects');
function fetchAll() { function fetchAll() {
return fetch(URI, { credentials: 'include' }) return fetch(URI, { credentials: 'include' })

View File

@ -20,22 +20,31 @@ export const ERROR_REMOVING_STRATEGY = 'ERROR_REMOVING_STRATEGY';
export const ERROR_DEPRECATING_STRATEGY = 'ERROR_DEPRECATING_STRATEGY'; export const ERROR_DEPRECATING_STRATEGY = 'ERROR_DEPRECATING_STRATEGY';
export const ERROR_REACTIVATING_STRATEGY = 'ERROR_REACTIVATING_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 addStrategy = strategy => ({ type: ADD_STRATEGY, strategy });
const createRemoveStrategy = strategy => ({ type: REMOVE_STRATEGY, strategy }); const createRemoveStrategy = strategy => ({ type: REMOVE_STRATEGY, strategy });
const updatedStrategy = strategy => ({ type: UPDATE_STRATEGY, strategy }); const updatedStrategy = strategy => ({ type: UPDATE_STRATEGY, strategy });
const startRequest = () => ({ type: REQUEST_STRATEGIES }); const startRequest = () => ({ type: REQUEST_STRATEGIES });
const receiveStrategies = json => ({ type: RECEIVE_STRATEGIES, value: json.strategies });
const startCreate = () => ({ type: START_CREATE_STRATEGY }); const startCreate = () => ({ type: START_CREATE_STRATEGY });
const startUpdate = () => ({ type: START_UPDATE_STRATEGY }); const startUpdate = () => ({ type: START_UPDATE_STRATEGY });
const startDeprecate = () => ({ type: START_DEPRECATE_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 startReactivate = () => ({ type: START_REACTIVATE_STRATEGY });
const reactivateStrategyEvent = strategy => ({ type: REACTIVATE_STRATEGY, strategy }); const reactivateStrategyEvent = strategy => ({
type: REACTIVATE_STRATEGY,
strategy,
});
export function fetchStrategies() { export function fetchStrategies() {
return dispatch => { return dispatch => {
@ -43,7 +52,7 @@ export function fetchStrategies() {
return api return api
.fetchAll() .fetchAll()
.then(json => dispatch(receiveStrategies(json))) .then(json => dispatch(receiveStrategies(json.strategies)))
.catch(dispatchError(dispatch, ERROR_RECEIVE_STRATEGIES)); .catch(dispatchError(dispatch, ERROR_RECEIVE_STRATEGIES));
}; };
} }

View File

@ -1,6 +1,7 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess, headers } from '../api-helper'; import { throwIfNotSuccess, headers } from '../api-helper';
const URI = 'api/admin/strategies'; const URI = formatApiPath('api/admin/strategies');
function fetchAll() { function fetchAll() {
return fetch(URI, { credentials: 'include' }) return fetch(URI, { credentials: 'include' })

View File

@ -14,7 +14,7 @@ export const START_UPDATE_TAG_TYPE = 'START_UPDATE_TAG_TYPE';
export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE'; export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE';
export const ERROR_UPDATE_TAG_TYPE = 'ERROR_UPDATE_TAG_TYPE'; export const ERROR_UPDATE_TAG_TYPE = 'ERROR_UPDATE_TAG_TYPE';
function receiveTagTypes(json) { export function receiveTagTypes(json) {
return { return {
type: RECEIVE_TAG_TYPES, type: RECEIVE_TAG_TYPES,
value: json, value: json,
@ -37,7 +37,12 @@ export function createTagType({ name, description, icon }) {
dispatch({ type: START_CREATE_TAG_TYPE }); dispatch({ type: START_CREATE_TAG_TYPE });
return api return api
.create({ name, description, icon }) .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)); .catch(dispatchError(dispatch, ERROR_CREATE_TAG_TYPE));
}; };
} }
@ -47,7 +52,12 @@ export function updateTagType({ name, description, icon }) {
dispatch({ type: START_UPDATE_TAG_TYPE }); dispatch({ type: START_UPDATE_TAG_TYPE });
return api return api
.update({ name, description, icon }) .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)); .catch(dispatchError(dispatch, ERROR_UPDATE_TAG_TYPE));
}; };
} }

View File

@ -1,6 +1,7 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess, headers } from '../api-helper'; import { throwIfNotSuccess, headers } from '../api-helper';
const URI = 'api/admin/tag-types'; const URI = formatApiPath('api/admin/tag-types');
function fetchTagTypes() { function fetchTagTypes() {
return fetch(URI, { credentials: 'include' }) return fetch(URI, { credentials: 'include' })
@ -53,5 +54,5 @@ const api = {
update, update,
deleteTagType, deleteTagType,
validateTagType, validateTagType,
} };
export default api; export default api;

View File

@ -1,6 +1,7 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess, headers } from '../api-helper'; import { throwIfNotSuccess, headers } from '../api-helper';
const URI = 'api/admin/tags'; const URI = formatApiPath('api/admin/tags');
function fetchTags() { function fetchTags() {
return fetch(URI, { credentials: 'include' }) return fetch(URI, { credentials: 'include' })

View 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));
}

View 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,
};

View File

@ -1,6 +1,7 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess } from '../api-helper'; import { throwIfNotSuccess } from '../api-helper';
const URI = 'api/admin/ui-config'; const URI = formatApiPath('api/admin/ui-config');
function fetchConfig() { function fetchConfig() {
return fetch(URI, { credentials: 'include' }) return fetch(URI, { credentials: 'include' })

View File

@ -1,6 +1,7 @@
import api from './api'; import api from './api';
import { dispatchError } from '../util'; import { dispatchError } from '../util';
import { RESET_LOADING } from '../feature-toggle/actions'; import { RESET_LOADING } from '../feature-toggle/actions';
import { getBasePath } from '../../utils/format-path';
export const USER_CHANGE_CURRENT = 'USER_CHANGE_CURRENT'; export const USER_CHANGE_CURRENT = 'USER_CHANGE_CURRENT';
export const USER_LOGOUT = 'USER_LOGOUT'; export const USER_LOGOUT = 'USER_LOGOUT';
export const USER_LOGIN = 'USER_LOGIN'; export const USER_LOGIN = 'USER_LOGIN';
@ -63,12 +64,15 @@ export function passwordLogin(path, user) {
} }
export function logoutUser() { export function logoutUser() {
const basepath = getBasePath();
return dispatch => { return dispatch => {
return api return api
.logoutUser() .logoutUser()
.then(() => dispatch({ type: USER_LOGOUT })) .then(() => dispatch({ type: USER_LOGOUT }))
.then(() => dispatch({ type: RESET_LOADING })) .then(() => dispatch({ type: RESET_LOADING }))
.then(() => (window.location = '/')) .then(() => {
window.location = `${basepath}`;
})
.catch(handleError); .catch(handleError);
}; };
} }

View File

@ -1,51 +1,52 @@
import { throwIfNotSuccess, headers } from "../api-helper"; import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess, headers } from '../api-helper';
const URI = "api/admin/user"; const URI = formatApiPath('api/admin/user');
function logoutUser() { function logoutUser() {
return fetch(`logout`, { return fetch(formatApiPath('logout'), {
method: "GET", method: 'GET',
credentials: "include", credentials: 'include',
}).then(throwIfNotSuccess); }).then(throwIfNotSuccess);
} }
function fetchUser() { function fetchUser() {
return fetch(URI, { credentials: "include" }) return fetch(URI, { credentials: 'include' })
.then(throwIfNotSuccess) .then(throwIfNotSuccess)
.then((response) => response.json()); .then(response => response.json());
} }
function insecureLogin(path, user) { function insecureLogin(path, user) {
return fetch(path, { return fetch(path, {
method: "POST", method: 'POST',
credentials: "include", credentials: 'include',
headers, headers,
body: JSON.stringify(user), body: JSON.stringify(user),
}) })
.then(throwIfNotSuccess) .then(throwIfNotSuccess)
.then((response) => response.json()); .then(response => response.json());
} }
function demoLogin(path, user) { function demoLogin(path, user) {
return fetch(path, { return fetch(path, {
method: "POST", method: 'POST',
credentials: "include", credentials: 'include',
headers, headers,
body: JSON.stringify(user), body: JSON.stringify(user),
}) })
.then(throwIfNotSuccess) .then(throwIfNotSuccess)
.then((response) => response.json()); .then(response => response.json());
} }
function passwordLogin(path, data) { function passwordLogin(path, data) {
return fetch(path, { return fetch(path, {
method: "POST", method: 'POST',
credentials: "include", credentials: 'include',
headers, headers,
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
.then(throwIfNotSuccess) .then(throwIfNotSuccess)
.then((response) => response.json()); .then(response => response.json());
} }
const api = { const api = {

Some files were not shown because too many files have changed in this diff Show More