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

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

View File

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

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 AccessContext from '../../../contexts/AccessContext';
import slackIcon from '../../../assets/icons/slack.svg';
import jiraIcon from '../../../assets/icons/jira.svg';
import webhooksIcon from '../../../assets/icons/webhooks.svg';
import teamsIcon from '../../../assets/icons/teams.svg';
import dataDogIcon from '../../../assets/icons/datadog.svg';
import { formatAssetPath } from '../../../utils/format-path';
const style = {
width: '40px',
height: '40px',
@ -16,15 +23,45 @@ const style = {
const getIcon = name => {
switch (name) {
case 'slack':
return <img style={style} alt="Slack Logo" src="slack.svg" />;
return (
<img
style={style}
alt="Slack Logo"
src={formatAssetPath(slackIcon)}
/>
);
case 'jira-comment':
return <img style={style} alt="JIRA Logo" src="jira.svg" />;
return (
<img
style={style}
alt="JIRA Logo"
src={formatAssetPath(jiraIcon)}
/>
);
case 'webhook':
return <img style={style} alt="Generic Webhook logo" src="webhooks.svg" />;
return (
<img
style={style}
alt="Generic Webhook logo"
src={formatAssetPath(webhooksIcon)}
/>
);
case 'teams':
return <img style={style} alt="Microsoft Teams Logo" src="teams.svg" />;
return (
<img
style={style}
alt="Microsoft Teams Logo"
src={formatAssetPath(teamsIcon)}
/>
);
case 'datadog':
return <img style={style} alt="Datadog" src="datadog.svg" />;
return (
<img
style={style}
alt="Datadog"
src={formatAssetPath(dataDogIcon)}
/>
);
default:
return (
<Avatar>
@ -34,7 +71,14 @@ const getIcon = name => {
}
};
const AddonList = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, history }) => {
const AddonList = ({
addons,
providers,
fetchAddons,
removeAddon,
toggleAddon,
history,
}) => {
const { hasAccess } = useContext(AccessContext);
useEffect(() => {
if (addons.length === 0) {
@ -59,7 +103,12 @@ const AddonList = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, h
/>
<br />
<AvailableAddons providers={providers} hasAccess={hasAccess} history={history} getIcon={getIcon} />
<AvailableAddons
providers={providers}
hasAccess={hasAccess}
history={history}
getIcon={getIcon}
/>
</>
);
};

View File

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

View File

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

View File

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

View File

@ -2,9 +2,20 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Avatar, Link, Icon, IconButton, Button, LinearProgress, Typography } from '@material-ui/core';
import {
Avatar,
Link,
Icon,
IconButton,
Button,
LinearProgress,
Typography,
} from '@material-ui/core';
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
import { formatFullDateTimeWithLocale, formatDateWithLocale } from '../common/util';
import {
formatFullDateTimeWithLocale,
formatDateWithLocale,
} from '../common/util';
import { UPDATE_APPLICATION } from '../AccessProvider/permissions';
import ApplicationView from './application-view';
import ApplicationUpdate from './application-update';
@ -37,9 +48,12 @@ class ClientApplications extends PureComponent {
}
componentDidMount() {
this.props.fetchApplication(this.props.appName).finally(() => this.setState({ loading: false }));
this.props
.fetchApplication(this.props.appName)
.finally(() => this.setState({ loading: false }));
}
formatFullDateTime = v => formatFullDateTimeWithLocale(v, this.props.location.locale);
formatFullDateTime = v =>
formatFullDateTimeWithLocale(v, this.props.location.locale);
formatDate = v => formatDateWithLocale(v, this.props.location.locale);
deleteApplication = async evt => {
@ -64,7 +78,16 @@ class ClientApplications extends PureComponent {
}
const { hasAccess } = this.context;
const { application, storeApplicationMetaData } = this.props;
const { appName, instances, strategies, seenToggles, url, description, icon = 'apps', createdAt } = application;
const {
appName,
instances,
strategies,
seenToggles,
url,
description,
icon = 'apps',
createdAt,
} = application;
const toggleModal = () => {
this.setState(prev => ({ ...prev, prompt: !prev.prompt }));
@ -95,7 +118,10 @@ class ClientApplications extends PureComponent {
{
label: 'Edit application',
component: (
<ApplicationUpdate application={application} storeApplicationMetaData={storeApplicationMetaData} />
<ApplicationUpdate
application={application}
storeApplicationMetaData={storeApplicationMetaData}
/>
),
},
];
@ -131,7 +157,11 @@ class ClientApplications extends PureComponent {
<ConditionallyRender
condition={hasAccess(UPDATE_APPLICATION)}
show={
<Button color="secondary" title="Delete application" onClick={toggleModal}>
<Button
color="secondary"
title="Delete application"
onClick={toggleModal}
>
Delete
</Button>
}

View File

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

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}
render={props => {
if (unauthorized) {
return <Redirect to="/login" />;
return <Redirect to={'/login'} />;
} else {
return <Component {...props} {...renderProps} />;
}

View File

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

View File

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

View File

@ -27,7 +27,15 @@ const FeatureToggleListItem = ({
}) => {
const styles = useStyles();
const { name, description, enabled, type, stale, createdAt, project } = feature;
const {
name,
description,
enabled,
type,
stale,
createdAt,
project,
} = feature;
const { showLastHour = false } = settings;
const isStale = showLastHour
? metricsLastHour.isFallback

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,10 @@ import {
import HeaderTitle from '../../common/HeaderTitle';
import PageContent from '../../common/PageContent/PageContent';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import { CREATE_TAG_TYPE, DELETE_TAG_TYPE } from '../../AccessProvider/permissions';
import {
CREATE_TAG_TYPE,
DELETE_TAG_TYPE,
} from '../../AccessProvider/permissions';
import Dialogue from '../../common/Dialogue/Dialogue';
import useMediaQuery from '@material-ui/core/useMediaQuery';
@ -28,7 +31,6 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
const [deletion, setDeletion] = useState({ open: false });
const history = useHistory();
const smallScreen = useMediaQuery('(max-width:700px)');
useEffect(() => {
fetchTagTypes();
@ -48,7 +50,9 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
<Tooltip title="Add tag type">
<IconButton
aria-label="add tag type"
onClick={() => history.push('/tag-types/create')}
onClick={() =>
history.push('/tag-types/create')
}
>
<Icon>add</Icon>
</IconButton>
@ -58,7 +62,9 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
<Button
variant="contained"
color="primary"
onClick={() => history.push('/tag-types/create')}
onClick={() =>
history.push('/tag-types/create')
}
>
Add new tag type
</Button>
@ -91,12 +97,18 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
</Tooltip>
);
return (
<ListItem key={`${tagType.name}`} classes={{ root: styles.tagListItem }}>
<ListItem
key={`${tagType.name}`}
classes={{ root: styles.tagListItem }}
>
<ListItemIcon>
<Icon>label</Icon>
</ListItemIcon>
<ListItemText primary={link} secondary={tagType.description} />
<ConditionallyRender condition={hasAccess(DELETE_TAG_TYPE)} show={deleteButton} />
<ConditionallyRender
condition={hasAccess(DELETE_TAG_TYPE)}
show={deleteButton}
/>
</ListItem>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,12 @@ const initialState = {
unleashHostname: location.hostname,
};
function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl }) {
function GoogleAuth({
config,
getGoogleConfig,
updateGoogleConfig,
unleashUrl,
}) {
const [data, setData] = useState(initialState);
const [info, setInfo] = useState();
const { hasAccess } = useContext(AccessContext);
@ -64,11 +69,16 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
<Grid item xs={12}>
<Alert severity="info">
Please read the{' '}
<a href="https://www.unleash-hosted.com/docs/enterprise-authentication/google" target="_blank" rel="noreferrer">
<a
href="https://www.unleash-hosted.com/docs/enterprise-authentication/google"
target="_blank"
rel="noreferrer"
>
documentation
</a>{' '}
to learn how to integrate with Google OAuth 2.0. <br />
Callback URL: <code>{unleashUrl}/auth/google/callback</code>
Callback URL:{' '}
<code>{unleashUrl}/auth/google/callback</code>
</Alert>
</Grid>
</Grid>
@ -77,12 +87,16 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
<Grid item xs={5}>
<strong>Enable</strong>
<p>
Enable Google users to login. Value is ignored if Client ID and Client Secret are not
defined.
Enable Google users to login. Value is ignored if
Client ID and Client Secret are not defined.
</p>
</Grid>
<Grid item xs={6} style={{ padding: '20px' }}>
<Switch onChange={updateEnabled} name="enabled" checked={data.enabled}>
<Switch
onChange={updateEnabled}
name="enabled"
checked={data.enabled}
>
{data.enabled ? 'Enabled' : 'Disabled'}
</Switch>
</Grid>
@ -90,7 +104,10 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
<Grid container spacing={3}>
<Grid item xs={5}>
<strong>Client ID</strong>
<p>(Required) The Client ID provided by Google when registering the application.</p>
<p>
(Required) The Client ID provided by Google when
registering the application.
</p>
</Grid>
<Grid item xs={6}>
<TextField
@ -108,7 +125,10 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Client Secret</strong>
<p>(Required) Client Secret provided by Google when registering the application.</p>
<p>
(Required) Client Secret provided by Google when
registering the application.
</p>
</Grid>
<Grid item md={6}>
<TextField
@ -127,10 +147,13 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
<Grid item md={5}>
<strong>Unleash hostname</strong>
<p>
(Required) The hostname you are running Unleash on that Google should send the user back to.
The final callback URL will be{' '}
(Required) The hostname you are running Unleash on
that Google should send the user back to. The final
callback URL will be{' '}
<small>
<code>https://[unleash.hostname.com]/auth/google/callback</code>
<code>
https://[unleash.hostname.com]/auth/google/callback
</code>
</small>
</p>
</Grid>
@ -150,10 +173,17 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Auto-create users</strong>
<p>Enable automatic creation of new users when signing in with Google.</p>
<p>
Enable automatic creation of new users when signing
in with Google.
</p>
</Grid>
<Grid item md={6} style={{ padding: '20px' }}>
<Switch onChange={updateAutoCreate} name="enabled" checked={data.autoCreate}>
<Switch
onChange={updateAutoCreate}
name="enabled"
checked={data.autoCreate}
>
Auto-create users
</Switch>
</Grid>
@ -161,7 +191,10 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Email domains</strong>
<p>(Optional) Comma separated list of email domains that should be allowed to sign in.</p>
<p>
(Optional) Comma separated list of email domains
that should be allowed to sign in.
</p>
</Grid>
<Grid item md={6}>
<TextField
@ -180,7 +213,11 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl })
</Grid>
<Grid container spacing={3}>
<Grid item md={5}>
<Button variant="contained" color="primary" type="submit">
<Button
variant="contained"
color="primary"
type="submit"
>
Save
</Button>{' '}
<small>{info}</small>

View File

@ -4,7 +4,7 @@ import { Button, Grid, Switch, TextField } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import PageContent from '../../../component/common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../../component/AccessProvider/permissions';
import { ADMIN } from '../../../component/AccessProvider/permissions';
const initialState = {
enabled: false,
@ -30,7 +30,11 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
}, [config]);
if (!hasAccess(ADMIN)) {
return <Alert severity="error">You need to be a root admin to access this section.</Alert>;
return (
<Alert severity="error">
You need to be a root admin to access this section.
</Alert>
);
}
const updateField = e => {
@ -65,11 +69,17 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
<Grid item md={12}>
<Alert severity="info">
Please read the{' '}
<a href="https://www.unleash-hosted.com/docs/enterprise-authentication" target="_blank" rel="noreferrer">
<a
href="https://www.unleash-hosted.com/docs/enterprise-authentication"
target="_blank"
rel="noreferrer"
>
documentation
</a>{' '}
to learn how to integrate with specific SAML 2.0 providers (Okta, Keycloak, etc). <br />
Callback URL: <code>{unleashUrl}/auth/saml/callback</code>
to learn how to integrate with specific SAML 2.0
providers (Okta, Keycloak, etc). <br />
Callback URL:{' '}
<code>{unleashUrl}/auth/saml/callback</code>
</Alert>
</Grid>
</Grid>
@ -80,7 +90,12 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
<p>Enable SAML 2.0 Authentication.</p>
</Grid>
<Grid item md={6}>
<Switch onChange={updateEnabled} value={data.enabled} name="enabled" checked={data.enabled}>
<Switch
onChange={updateEnabled}
value={data.enabled}
name="enabled"
checked={data.enabled}
>
{data.enabled ? 'Enabled' : 'Disabled'}
</Switch>
</Grid>
@ -105,7 +120,10 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Single Sign-On URL</strong>
<p>(Required) The url to redirect the user to for signing in.</p>
<p>
(Required) The url to redirect the user to for
signing in.
</p>
</Grid>
<Grid item md={6}>
<TextField
@ -122,7 +140,10 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
<Grid container spacing={3}>
<Grid item md={5}>
<strong>X.509 Certificate</strong>
<p>(Required) The certificate used to sign the SAML 2.0 request.</p>
<p>
(Required) The certificate used to sign the SAML 2.0
request.
</p>
</Grid>
<Grid item md={7}>
<TextField
@ -146,10 +167,17 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Auto-create users</strong>
<p>Enable automatic creation of new users when signing in with Saml.</p>
<p>
Enable automatic creation of new users when signing
in with Saml.
</p>
</Grid>
<Grid item md={6} style={{ padding: '20px' }}>
<Switch onChange={updateAutoCreate} name="enabled" checked={data.autoCreate}>
<Switch
onChange={updateAutoCreate}
name="enabled"
checked={data.autoCreate}
>
Auto-create users
</Switch>
</Grid>
@ -157,7 +185,10 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Email domains</strong>
<p>(Optional) Comma separated list of email domains that should be allowed to sign in.</p>
<p>
(Optional) Comma separated list of email domains
that should be allowed to sign in.
</p>
</Grid>
<Grid item md={6}>
<TextField
@ -175,7 +206,11 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
</Grid>
<Grid container spacing={3}>
<Grid item md={5}>
<Button variant="contained" color="primary" type="submit">
<Button
variant="contained"
color="primary"
type="submit"
>
Save
</Button>{' '}
<small>{info}</small>

View File

@ -1,7 +1,7 @@
import { Typography } from '@material-ui/core';
import Dialogue from '../../../../../component/common/Dialogue';
import { ReactComponent as EmailIcon } from '../../../../../icons/email.svg';
import { ReactComponent as EmailIcon } from '../../../../../assets/icons/email.svg';
import { useStyles } from './ConfirmUserEmail.styles';
interface IConfirmUserEmailProps {

View File

@ -251,4 +251,4 @@ UsersList.propTypes = {
location: PropTypes.object.isRequired,
};
export default UsersList;
export default UsersList;

View File

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

View File

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

View File

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

View File

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

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 ERROR_UPDATE_CONTEXT_FIELD = 'ERROR_UPDATE_CONTEXT_FIELD';
const receiveContext = value => ({ type: RECEIVE_CONTEXT, value });
export const receiveContext = value => ({ type: RECEIVE_CONTEXT, value });
const addContextField = context => ({ type: ADD_CONTEXT_FIELD, context });
const upContextField = context => ({ type: UPDATE_CONTEXT_FIELD, context });
const createRemoveContext = context => ({ type: REMOVE_CONTEXT, context });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,12 +4,15 @@ import { dispatchError } from '../util';
export const RECEIVE_FEATURE_TYPES = 'RECEIVE_FEATURE_TYPES';
export const ERROR_RECEIVE_FEATURE_TYPES = 'ERROR_RECEIVE_FEATURE_TYPES';
const receiveFeatureTypes = value => ({ type: RECEIVE_FEATURE_TYPES, value });
export const receiveFeatureTypes = value => ({
type: RECEIVE_FEATURE_TYPES,
value,
});
export function fetchFeatureTypes() {
return dispatch =>
api
.fetchAll()
.then(json => dispatch(receiveFeatureTypes(json)))
.then(json => dispatch(receiveFeatureTypes(json.types)))
.catch(dispatchError(dispatch, ERROR_RECEIVE_FEATURE_TYPES));
}

View File

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

View File

@ -1,7 +1,9 @@
import { List } from 'immutable';
import { RECEIVE_FEATURE_TYPES } from './actions';
const DEFAULT_FEATURE_TYPES = [{ id: 'release', name: 'Release', initial: true }];
const DEFAULT_FEATURE_TYPES = [
{ id: 'release', name: 'Release', initial: true },
];
function getInitState() {
return new List(DEFAULT_FEATURE_TYPES);
@ -10,7 +12,7 @@ function getInitState() {
const strategies = (state = getInitState(), action) => {
switch (action.type) {
case RECEIVE_FEATURE_TYPES:
return new List(action.value.types);
return new List(action.value);
default:
return state;
}

View File

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

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 upProject = project => ({ type: UPDATE_PROJECT, project });
const delProject = project => ({ type: REMOVE_PROJECT, project });
export const receiveProjects = value => ({ type: RECEIVE_PROJECT, value });
export function fetchProjects() {
const receiveProjects = value => ({ type: RECEIVE_PROJECT, value });
return dispatch =>
api
.fetchAll()

View File

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

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

View File

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

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

View File

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

View File

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

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';
const URI = 'api/admin/ui-config';
const URI = formatApiPath('api/admin/ui-config');
function fetchConfig() {
return fetch(URI, { credentials: 'include' })

View File

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

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