From f0d6e45361761c17199b7c648c215cad9882ea04 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Tue, 4 May 2021 09:59:42 +0200 Subject: [PATCH] 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 --- frontend/public/index.html | 33 +-- frontend/src/assets/icons/datadog.svg | 1 + frontend/src/{ => assets}/icons/email.svg | 0 frontend/src/assets/icons/jira.svg | 1 + frontend/src/assets/icons/slack.svg | 1 + frontend/src/{ => assets}/icons/star.svg | 0 frontend/src/{ => assets}/icons/switches.svg | 0 frontend/src/assets/icons/teams.svg | 59 ++++++ .../src/{ => assets}/icons/toggleLeft.svg | 0 .../src/{ => assets}/icons/toggleRight.svg | 0 .../icons/unleash-logo-inverted.svg | 0 frontend/src/assets/icons/webhooks.svg | 1 + frontend/src/assets/img/logo.png | Bin 0 -> 2492 bytes .../AccessProvider/AccessProvider.tsx | 50 ++--- frontend/src/component/App.tsx | 9 +- frontend/src/component/AppContainer.tsx | 14 ++ .../component/addons/AddonList/AddonList.jsx | 63 +++++- .../AvailableAddons/AvailableAddons.jsx | 18 +- .../application-edit-component-test.js.snap | 3 +- .../application-edit-component-test.js | 191 +++++++++--------- .../application/application-edit-component.js | 44 +++- .../application/application-view.jsx | 132 +++++++----- .../Proclamation/Proclamation.styles.ts | 15 ++ .../common/Proclamation/Proclamation.tsx | 68 +++++++ .../common/ProtectedRoute/ProtectedRoute.jsx | 2 +- .../context/ContextList/ContextList.jsx | 26 ++- .../context/form-context-component.jsx | 1 - .../FeatureToggleListItem.jsx | 10 +- .../__tests__/list-component-test.jsx | 50 ++--- .../feature/FeatureView/FeatureView.jsx | 6 +- .../feature/create/copy-feature-component.jsx | 37 +++- .../feature/feature-tag-component.jsx | 37 +++- .../feature/feature-type-select-component.jsx | 14 +- .../view/__tests__/view-component-test.jsx | 32 +-- .../layout/LayoutPicker/LayoutPicker.jsx | 2 +- .../layout/MainLayout/MainLayout.jsx | 4 +- .../src/component/layout/MainLayout/index.js | 10 + frontend/src/component/menu/Header/Header.jsx | 10 +- frontend/src/component/menu/Header/index.jsx | 5 +- frontend/src/component/menu/breadcrumb.jsx | 12 +- frontend/src/component/menu/drawer.jsx | 46 ++++- .../project/ProjectList/ProjectList.jsx | 44 +++- .../__tests__/list-component-test.jsx | 20 +- .../strategy-details-component-test.jsx | 4 +- .../strategies/strategy-details-component.jsx | 20 +- .../tag-types/TagTypeList/TagTypeList.jsx | 24 ++- .../tag-type-create-component-test.js | 67 +++--- .../__tests__/tag-type-list-component-test.js | 25 ++- .../tag-types/form-tag-type-component.js | 56 ++++- .../src/component/tags/TagList/TagList.jsx | 34 +++- .../src/component/user/DemoAuth/DemoAuth.jsx | 36 ++-- .../ForgottenPassword/ForgottenPassword.tsx | 4 +- frontend/src/component/user/Login/Login.jsx | 9 +- frontend/src/component/user/Login/index.js | 7 +- .../user/PasswordAuth/PasswordAuth.jsx | 5 +- .../component/user/SimpleAuth/SimpleAuth.jsx | 12 +- .../StandaloneBanner/StandaloneBanner.tsx | 6 +- .../UserProfile/EditProfile/EditProfile.tsx | 4 +- .../user/UserProfile/UserProfile.jsx | 1 - .../UserProfileContent/UserProfileContent.jsx | 1 - .../user/authentication-component.jsx | 4 - .../user/authentication-container.jsx | 13 +- .../PasswordChecker/PasswordChecker.tsx | 6 +- .../ResetPasswordForm/ResetPasswordForm.tsx | 4 +- frontend/src/hooks/useAdminUsersApi.ts | 21 +- frontend/src/hooks/useResetPassword.ts | 14 +- frontend/src/hooks/useUsers.ts | 7 +- frontend/src/index.tsx | 9 +- frontend/src/page/admin/api/api-key-list.jsx | 65 ++++-- frontend/src/page/admin/auth/google-auth.jsx | 67 ++++-- frontend/src/page/admin/auth/saml-auth.jsx | 59 ++++-- .../ConfirmUserEmail/ConfirmUserEmail.tsx | 2 +- .../page/admin/users/UsersList/UsersList.jsx | 2 +- frontend/src/page/admin/users/index.js | 20 +- frontend/src/store/addons/api.js | 3 +- frontend/src/store/application/api.js | 3 +- frontend/src/store/archive/api.js | 3 +- frontend/src/store/context/actions.js | 2 +- frontend/src/store/context/api.js | 3 +- frontend/src/store/context/index.js | 11 +- frontend/src/store/e-api-admin/api.js | 3 +- frontend/src/store/feature-metrics/api.js | 5 +- frontend/src/store/feature-tags/api.js | 18 +- frontend/src/store/feature-toggle/api.js | 8 +- frontend/src/store/feature-type/actions.js | 7 +- frontend/src/store/feature-type/api.js | 3 +- frontend/src/store/feature-type/index.js | 6 +- frontend/src/store/history/api.js | 3 +- frontend/src/store/loader.js | 18 -- frontend/src/store/project/actions.js | 2 +- frontend/src/store/project/api.js | 3 +- frontend/src/store/strategy/actions.js | 19 +- frontend/src/store/strategy/api.js | 3 +- frontend/src/store/tag-type/actions.js | 16 +- frontend/src/store/tag-type/api.js | 5 +- frontend/src/store/tag/api.js | 3 +- frontend/src/store/ui-bootstrap/actions.js | 26 +++ frontend/src/store/ui-bootstrap/api.js | 14 ++ frontend/src/store/ui-config/api.js | 3 +- frontend/src/store/user/actions.js | 6 +- frontend/src/store/user/api.js | 33 +-- frontend/src/utils/format-path.ts | 51 +++++ 102 files changed, 1390 insertions(+), 569 deletions(-) create mode 100644 frontend/src/assets/icons/datadog.svg rename frontend/src/{ => assets}/icons/email.svg (100%) create mode 100644 frontend/src/assets/icons/jira.svg create mode 100644 frontend/src/assets/icons/slack.svg rename frontend/src/{ => assets}/icons/star.svg (100%) rename frontend/src/{ => assets}/icons/switches.svg (100%) create mode 100644 frontend/src/assets/icons/teams.svg rename frontend/src/{ => assets}/icons/toggleLeft.svg (100%) rename frontend/src/{ => assets}/icons/toggleRight.svg (100%) rename frontend/src/{ => assets}/icons/unleash-logo-inverted.svg (100%) create mode 100644 frontend/src/assets/icons/webhooks.svg create mode 100644 frontend/src/assets/img/logo.png create mode 100644 frontend/src/component/AppContainer.tsx create mode 100644 frontend/src/component/common/Proclamation/Proclamation.styles.ts create mode 100644 frontend/src/component/common/Proclamation/Proclamation.tsx create mode 100644 frontend/src/component/layout/MainLayout/index.js delete mode 100644 frontend/src/store/loader.js create mode 100644 frontend/src/store/ui-bootstrap/actions.js create mode 100644 frontend/src/store/ui-bootstrap/api.js create mode 100644 frontend/src/utils/format-path.ts diff --git a/frontend/public/index.html b/frontend/public/index.html index c62ab5fe17..ad4d9f93c8 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -1,17 +1,24 @@ - - - - - - + + + + + + + - Unleash - Enterprise ready feature toggles - - - - -
- + Unleash - Enterprise ready feature toggles + + + + +
+ diff --git a/frontend/src/assets/icons/datadog.svg b/frontend/src/assets/icons/datadog.svg new file mode 100644 index 0000000000..49e6f8473f --- /dev/null +++ b/frontend/src/assets/icons/datadog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/icons/email.svg b/frontend/src/assets/icons/email.svg similarity index 100% rename from frontend/src/icons/email.svg rename to frontend/src/assets/icons/email.svg diff --git a/frontend/src/assets/icons/jira.svg b/frontend/src/assets/icons/jira.svg new file mode 100644 index 0000000000..4ace5cc84a --- /dev/null +++ b/frontend/src/assets/icons/jira.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/icons/slack.svg b/frontend/src/assets/icons/slack.svg new file mode 100644 index 0000000000..69a4eb6a21 --- /dev/null +++ b/frontend/src/assets/icons/slack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/icons/star.svg b/frontend/src/assets/icons/star.svg similarity index 100% rename from frontend/src/icons/star.svg rename to frontend/src/assets/icons/star.svg diff --git a/frontend/src/icons/switches.svg b/frontend/src/assets/icons/switches.svg similarity index 100% rename from frontend/src/icons/switches.svg rename to frontend/src/assets/icons/switches.svg diff --git a/frontend/src/assets/icons/teams.svg b/frontend/src/assets/icons/teams.svg new file mode 100644 index 0000000000..90b3444b60 --- /dev/null +++ b/frontend/src/assets/icons/teams.svg @@ -0,0 +1,59 @@ + + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/icons/toggleLeft.svg b/frontend/src/assets/icons/toggleLeft.svg similarity index 100% rename from frontend/src/icons/toggleLeft.svg rename to frontend/src/assets/icons/toggleLeft.svg diff --git a/frontend/src/icons/toggleRight.svg b/frontend/src/assets/icons/toggleRight.svg similarity index 100% rename from frontend/src/icons/toggleRight.svg rename to frontend/src/assets/icons/toggleRight.svg diff --git a/frontend/src/icons/unleash-logo-inverted.svg b/frontend/src/assets/icons/unleash-logo-inverted.svg similarity index 100% rename from frontend/src/icons/unleash-logo-inverted.svg rename to frontend/src/assets/icons/unleash-logo-inverted.svg diff --git a/frontend/src/assets/icons/webhooks.svg b/frontend/src/assets/icons/webhooks.svg new file mode 100644 index 0000000000..ec5cddf369 --- /dev/null +++ b/frontend/src/assets/icons/webhooks.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/img/logo.png b/frontend/src/assets/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..27ff43307c139d59f951d7eab118e1d79278abcb GIT binary patch literal 2492 zcmb_e`8yQ)7oNq;SZ0vy#AR$(HTLa0_9asWgDl0kjO_|BwjriLWveV>tAM|yj zNyfM&+)DN(%#f0@E5_Ar?)3c|?s=Yb&U4=LoF6{#=REIo@^Eg>2vJ2*004kEiE;E4 zWcE)Nh60_=sNzlqySV%bn;8|iVaUB z1;>X0NF)*}@_bZ6NN`*jDmFfXwr;5?7_>O)Xm>iLa&0aq`t+G2FSqo(W*}!}g+yg& z*)f?TCLoBJ6w;>`QXY!LPm=8hp=B6#MTgqnelg>PE1?5gN*=@Vxmp@5UE|B%Bh6f- zqzZf;l?d!UADmkBq}216{@T`y2O_0}*yZ_zp;xmU?fp*L?H|jp_Bl6=Na3j5|HR;t zr5hm7DWD1XCgL>MC^L_39t6@?kzaenzPzH)kLQ|h#@0Z10CS1wVzXa;=7d8Nz?Krn zAmxB0wno>7eqnj;u}$%~lwY29`jjHAf$4JQ*Q*>gZAQdaVJuN;HFrO#F__GwG)q5a zLZV^!OXBWkRAh$mD7aEyf-3L0{#Bu~LY05=FV*mdz+6}E87XcE+uv4^f0!QXYYht2 z?%>=I`4G1HE{vspc3}NEqLk`<0&HWi6rh>X1lFU%1~cFDs&SfoROm&FI1SvoNKr4F zeRDp36V4Z#@>*bNyAKo^14D@H7X=49Q`fPKOAJk>IQS?dq6~RB37jsrba?jL>CbKB}i%Z9y58;q}{ zscEs&bMdrwtIjTf1G*^NZLW?af^QUBMV2#~5&CjzwNco9qNCv!jC)TXe0dst4YW%z z>r%IV)q4@L(}{%%@4z<^+)1?`ff}NdjOzb=8eA$gB13NotvYCT^ma*c6}UlKl*$z& zwi0{K@VkGWUuvqw`1p8fS(!(G@`k~Hyo0kdtg*4NwY4>FZ+*^>&(AR4owP^2+#1pX zk~aB1egrQZE~xyte|Yl7e9X+u4E718e)J=6>wWlN<;U+oer&j&x(_kfCt^t$1;PT| z|LL26_PFl7v#LH6O4oD;iTw+U_ERjf5-ubFkK(@+8-+>QiBb2bQvGk2L<-zHe2v=9v>tC3k+2?bsELM5qI+q^D zyffuQnHjFFt%VQQIyEi-q2jFjcJD&S>xo8xGJU!~DJh9LFK4{iFQF{k2tVcEaA~9M zGuGdqcGEB{{}HpJ@b1Q4W#YG>qlNX4ACDxGNC$!SZYCxs%*q~LsNQ^YUoai|4fz^x#I-O*aV?!oDa=zYcm$UQq^7UO#!}oohWhQ8qonqd2xtHVD<8TV{d`>P) zTxTJArgeZM!nkP^A^ymxG2S2%fsOBrHl#zlWRCjaJUtf)pKPN{F<|Lhy^fcJ0jeg;B^MtYtaepS zFWDe56oJc&0TH9Svmh*&&DtkVCSEibo3LLx$f^USWn?@&JQh0Odf`L2*9cSnrrqy< z;@C=a10tQzkJw%sKxHFZ&tEdmC^?dEiCT}14?FBk_p(@O6%^&UJwe^I zk7PFF0&M=4tOIWE)`n5{vhf`Q}}KFsAkaP-A!uK>ht3;H0rhhQ=*L zTPat>FM0O`ZLtvm`OPEAyxYLPb2>bVpr1s3yNX#|KjZ;U%%MAJBxg><6hSxRhqa4~ z)T1)4p=^Fdj!st0HkRK#xTxKKJ74X@1;w1&6aDB>;bk05Vuxoej z+{v7nm}tmhiKeT*6E0Q+CsHBK1L^LOkq?;3YqvJQtzKx-dZG!%CN+NL#IkQez7I5P zW#pb#mD|sauCz$#yLJRUF+K}!QG+3tg`I6H6?Afzdeb<%n_(MO2(@W$#KYnu4>8WdK zdSh&67V*zK_e~W#{9t!2e5N&$`0B4Pt^y%hC+p8LxR4v}s z!qEEU$;kAy!WX_uoTT5vdsT2WQmi2R6k+tLm1LR`l!yMIZGSSH<%;nL0L041iQ?V7 z7u!@1W;mA>H1b_aNI|3p`U(AVjHD_^56Blm!A8z1rvmr{i(b{F25{cNI`V^IU9p zjjXCQ?uasIOS0c>%ct>oO_9UAk#PDXwOnbk*zA zu^`ay+IR$vn&f=LGD#vWzC|hRpLW`mixj@sI_m0wa&as@ = ({store, children}) => { - const hasAccess = (permission: string, project: string) => { - const permissions = store.getState().user.get('permissions') || []; +const AccessProvider: FC = ({ 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 {children} -} + return ( + + {children} + + ); +}; -export default AccessProvider; \ No newline at end of file +export default AccessProvider; diff --git a/frontend/src/component/App.tsx b/frontend/src/component/App.tsx index 62627cc4ba..11910ce144 100644 --- a/frontend/src/component/App.tsx +++ b/frontend/src/component/App.tsx @@ -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); }; diff --git a/frontend/src/component/AppContainer.tsx b/frontend/src/component/AppContainer.tsx new file mode 100644 index 0000000000..00823c17fa --- /dev/null +++ b/frontend/src/component/AppContainer.tsx @@ -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); diff --git a/frontend/src/component/addons/AddonList/AddonList.jsx b/frontend/src/component/addons/AddonList/AddonList.jsx index ca719578f3..9e0b3ed109 100644 --- a/frontend/src/component/addons/AddonList/AddonList.jsx +++ b/frontend/src/component/addons/AddonList/AddonList.jsx @@ -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 Slack Logo; + return ( + Slack Logo + ); case 'jira-comment': - return JIRA Logo; + return ( + JIRA Logo + ); case 'webhook': - return Generic Webhook logo; + return ( + Generic Webhook logo + ); case 'teams': - return Microsoft Teams Logo; + return ( + Microsoft Teams Logo + ); case 'datadog': - return Datadog; + return ( + Datadog + ); default: return ( @@ -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 />
- + ); }; diff --git a/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.jsx b/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.jsx index 10606b0438..9f19deec45 100644 --- a/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.jsx +++ b/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.jsx @@ -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 => ( {getIcon(provider.name)} - + { } diff --git a/frontend/src/component/application/application-view.jsx b/frontend/src/component/application/application-view.jsx index c32994497c..a75391c0ae 100644 --- a/frontend/src/component/application/application-view.jsx +++ b/frontend/src/component/application/application-view.jsx @@ -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 }) => ( report {name}} + primary={ + {name} + } secondary={'Missing, want to create?'} /> @@ -27,14 +44,24 @@ function ApplicationView({ seenToggles, hasAccess, strategies, instances, format report - + } /> ); // eslint-disable-next-line react/prop-types - const foundListItem = ({ viewUrl, name, showSwitch, enabled, description, i }) => ( + const foundListItem = ({ + viewUrl, + name, + showSwitch, + enabled, + description, + i, + }) => ( {shorten(name, 50)}} + primary={ + {shorten(name, 50)} + } secondary={shorten(description, 60)} /> @@ -58,26 +87,28 @@ function ApplicationView({ seenToggles, hasAccess, strategies, instances, format
- {seenToggles.map(({ name, description, enabled, notFound }, i) => ( - - ))} + {seenToggles.map( + ({ name, description, enabled, notFound }, i) => ( + + ) + )} @@ -114,28 +145,33 @@ function ApplicationView({ seenToggles, hasAccess, strategies, instances, format
- {instances.map(({ instanceId, clientIp, lastSeen, sdkVersion }) => ( - - - timeline - - - } - secondary={ - - {clientIp} last seen at {formatFullDateTime(lastSeen)} - - } - /> - - ))} + {instances.map( + ({ instanceId, clientIp, lastSeen, sdkVersion }) => ( + + + timeline + + + } + secondary={ + + {clientIp} last seen at{' '} + + {formatFullDateTime(lastSeen)} + + + } + /> + + ) + )}
diff --git a/frontend/src/component/common/Proclamation/Proclamation.styles.ts b/frontend/src/component/common/Proclamation/Proclamation.styles.ts new file mode 100644 index 0000000000..5ecb76cdc1 --- /dev/null +++ b/frontend/src/component/common/Proclamation/Proclamation.styles.ts @@ -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', + }, +}); diff --git a/frontend/src/component/common/Proclamation/Proclamation.tsx b/frontend/src/component/common/Proclamation/Proclamation.tsx new file mode 100644 index 0000000000..175527bf1b --- /dev/null +++ b/frontend/src/component/common/Proclamation/Proclamation.tsx @@ -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 ( + + + {toast.message} + + + View more + + + } + /> + ); +}; + +export default Proclamation; diff --git a/frontend/src/component/common/ProtectedRoute/ProtectedRoute.jsx b/frontend/src/component/common/ProtectedRoute/ProtectedRoute.jsx index 94371efce9..710626db9e 100644 --- a/frontend/src/component/common/ProtectedRoute/ProtectedRoute.jsx +++ b/frontend/src/component/common/ProtectedRoute/ProtectedRoute.jsx @@ -11,7 +11,7 @@ const ProtectedRoute = ({ {...rest} render={props => { if (unauthorized) { - return ; + return ; } else { return ; } diff --git a/frontend/src/component/context/ContextList/ContextList.jsx b/frontend/src/component/context/ContextList/ContextList.jsx index 7d03957dd3..0ba5874cd8 100644 --- a/frontend/src/component/context/ContextList/ContextList.jsx +++ b/frontend/src/component/context/ContextList/ContextList.jsx @@ -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 ( - }> + + } + > 0} diff --git a/frontend/src/component/context/form-context-component.jsx b/frontend/src/component/context/form-context-component.jsx index 2b4514ebfe..925383dc38 100644 --- a/frontend/src/component/context/form-context-component.jsx +++ b/frontend/src/component/context/form-context-component.jsx @@ -282,7 +282,6 @@ class AddContextComponent extends Component { Read more

- {console.log(contextField.stickiness)} { 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 diff --git a/frontend/src/component/feature/FeatureToggleList/__tests__/list-component-test.jsx b/frontend/src/component/feature/FeatureToggleList/__tests__/list-component-test.jsx index eda6282fe6..cee89b6d6f 100644 --- a/frontend/src/component/feature/FeatureToggleList/__tests__/list-component-test.jsx +++ b/frontend/src/component/feature/FeatureToggleList/__tests__/list-component-test.jsx @@ -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( - - + + @@ -59,17 +59,19 @@ test('renders correctly with one feature without permissions', () => { const tree = renderer.create( - - + + diff --git a/frontend/src/component/feature/FeatureView/FeatureView.jsx b/frontend/src/component/feature/FeatureView/FeatureView.jsx index b3c83e8312..dcc879ed41 100644 --- a/frontend/src/component/feature/FeatureView/FeatureView.jsx +++ b/frontend/src/component/feature/FeatureView/FeatureView.jsx @@ -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 + ); case 'metrics': return ; case 'variants': diff --git a/frontend/src/component/feature/create/copy-feature-component.jsx b/frontend/src/component/feature/create/copy-feature-component.jsx index 13139e3539..a9966d6d26 100644 --- a/frontend/src/component/feature/create/copy-feature-component.jsx +++ b/frontend/src/component/feature/create/copy-feature-component.jsx @@ -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 ( - +

Copy {copyToggle.name}

- You are about to create a new feature toggle by cloning the configuration of feature - toggle  - {copyToggle.name}. 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  + + {copyToggle.name} + + . You must give the new feature toggle a unique name + before you can proceed.

- diff --git a/frontend/src/component/feature/feature-tag-component.jsx b/frontend/src/component/feature/feature-tag-component.jsx index c667a3b023..6d55927438 100644 --- a/frontend/src/component/feature/feature-tag-component.jsx +++ b/frontend/src/component/feature/feature-tag-component.jsx @@ -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 slack; + return ( + slack + ); case 'jira': - return jira; + return ( + jira + ); case 'webhook': - return webhook; + return ( + webhook + ); default: return label; } diff --git a/frontend/src/component/feature/feature-type-select-component.jsx b/frontend/src/component/feature/feature-type-select-component.jsx index c083ee905c..6b185ef969 100644 --- a/frontend/src/component/feature/feature-type-select-component.jsx +++ b/frontend/src/component/feature/feature-type-select-component.jsx @@ -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 ; + return ( + + ); } } diff --git a/frontend/src/component/feature/view/__tests__/view-component-test.jsx b/frontend/src/component/feature/view/__tests__/view-component-test.jsx index eb2d1ed9b6..4fe8a9c4ee 100644 --- a/frontend/src/component/feature/view/__tests__/view-component-test.jsx +++ b/frontend/src/component/feature/view/__tests__/view-component-test.jsx @@ -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', () => { - - + + diff --git a/frontend/src/component/layout/LayoutPicker/LayoutPicker.jsx b/frontend/src/component/layout/LayoutPicker/LayoutPicker.jsx index 00b51aeb67..d8e044722e 100644 --- a/frontend/src/component/layout/LayoutPicker/LayoutPicker.jsx +++ b/frontend/src/component/layout/LayoutPicker/LayoutPicker.jsx @@ -1,5 +1,5 @@ import ConditionallyRender from '../../common/ConditionallyRender'; -import MainLayout from '../MainLayout/MainLayout'; +import MainLayout from '../MainLayout'; const LayoutPicker = ({ children, location }) => { const standalonePages = () => { diff --git a/frontend/src/component/layout/MainLayout/MainLayout.jsx b/frontend/src/component/layout/MainLayout/MainLayout.jsx index 1181e96cab..a81d082715 100644 --- a/frontend/src/component/layout/MainLayout/MainLayout.jsx +++ b/frontend/src/component/layout/MainLayout/MainLayout.jsx @@ -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 }) => {
+ {children}
diff --git a/frontend/src/component/layout/MainLayout/index.js b/frontend/src/component/layout/MainLayout/index.js new file mode 100644 index 0000000000..04ae6cce09 --- /dev/null +++ b/frontend/src/component/layout/MainLayout/index.js @@ -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); diff --git a/frontend/src/component/menu/Header/Header.jsx b/frontend/src/component/menu/Header/Header.jsx index 0955a35cbd..aa3d326308 100644 --- a/frontend/src/component/menu/Header/Header.jsx +++ b/frontend/src/component/menu/Header/Header.jsx @@ -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, }; diff --git a/frontend/src/component/menu/Header/index.jsx b/frontend/src/component/menu/Header/index.jsx index 4c0762c7f3..ab7c566f66 100644 --- a/frontend/src/component/menu/Header/index.jsx +++ b/frontend/src/component/menu/Header/index.jsx @@ -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); diff --git a/frontend/src/component/menu/breadcrumb.jsx b/frontend/src/component/menu/breadcrumb.jsx index c0464286c6..92ae8b00ea 100644 --- a/frontend/src/component/menu/breadcrumb.jsx +++ b/frontend/src/component/menu/breadcrumb.jsx @@ -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 = () => ( renderRoute(params, route)} + render={({ match: { params } } = this.props) => + renderRoute(params, route) + } /> ))} diff --git a/frontend/src/component/menu/drawer.jsx b/frontend/src/component/menu/drawer.jsx index f3d9901445..3bed45a12f 100644 --- a/frontend/src/component/menu/drawer.jsx +++ b/frontend/src/component/menu/drawer.jsx @@ -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 ; + return ( + + ); } else { return {name}; } @@ -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} @@ -51,12 +62,29 @@ function renderLink(link, toggleDrawer) { } } -export const DrawerMenu = ({ links = [], title = 'Unleash', flags = {}, open = false, toggleDrawer }) => ( - toggleDrawer()}> +export const DrawerMenu = ({ + links = [], + title = 'Unleash', + flags = {}, + open = false, + toggleDrawer, +}) => ( + toggleDrawer()} + >
- Unleash Logo + Unleash Logo {title}
@@ -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 ))} - {links.map(l => renderLink(l, toggleDrawer))} + + {links.map(l => renderLink(l, toggleDrawer))} +
); diff --git a/frontend/src/component/project/ProjectList/ProjectList.jsx b/frontend/src/component/project/ProjectList/ProjectList.jsx index c365e34d8a..043c70fb8a 100644 --- a/frontend/src/component/project/ProjectList/ProjectList.jsx +++ b/frontend/src/component/project/ProjectList/ProjectList.jsx @@ -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={ - history.push('/projects/create')}> + history.push('/projects/create')} + > add @@ -40,8 +55,11 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history }) => { const mgmAccessButton = project => ( - - + + supervised_user_circle @@ -68,17 +86,27 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history }) => { folder_open - + - + )); return ( - }> + + } + > 0} diff --git a/frontend/src/component/strategies/__tests__/list-component-test.jsx b/frontend/src/component/strategies/__tests__/list-component-test.jsx index 41f8e35010..02872e8ff1 100644 --- a/frontend/src/component/strategies/__tests__/list-component-test.jsx +++ b/frontend/src/component/strategies/__tests__/list-component-test.jsx @@ -42,15 +42,17 @@ test('renders correctly with one strategy without permissions', () => { const tree = renderer.create( - - + + diff --git a/frontend/src/component/strategies/__tests__/strategy-details-component-test.jsx b/frontend/src/component/strategies/__tests__/strategy-details-component-test.jsx index 5d22a32a82..9e560ace50 100644 --- a/frontend/src/component/strategies/__tests__/strategy-details-component-test.jsx +++ b/frontend/src/component/strategies/__tests__/strategy-details-component-test.jsx @@ -36,7 +36,7 @@ test('renders correctly with one strategy', () => { const tree = renderer.create( - + { fetchFeatureToggles={jest.fn()} history={{}} /> - + ); diff --git a/frontend/src/component/strategies/strategy-details-component.jsx b/frontend/src/component/strategies/strategy-details-component.jsx index 0bc2aad017..e62b0beb72 100644 --- a/frontend/src/component/strategies/strategy-details-component.jsx +++ b/frontend/src/component/strategies/strategy-details-component.jsx @@ -51,7 +51,13 @@ export default class StrategyDetails extends Component { }, { label: 'Edit', - component: , + component: ( + + ), }, ]; @@ -61,9 +67,13 @@ export default class StrategyDetails extends Component { - {strategy.description} + + {strategy.description} + @@ -75,7 +85,9 @@ export default class StrategyDetails extends Component {
diff --git a/frontend/src/component/tag-types/TagTypeList/TagTypeList.jsx b/frontend/src/component/tag-types/TagTypeList/TagTypeList.jsx index 8e294653dd..aacb9af926 100644 --- a/frontend/src/component/tag-types/TagTypeList/TagTypeList.jsx +++ b/frontend/src/component/tag-types/TagTypeList/TagTypeList.jsx @@ -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 }) => { history.push('/tag-types/create')} + onClick={() => + history.push('/tag-types/create') + } > add @@ -58,7 +62,9 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => { @@ -91,12 +97,18 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => { ); return ( - + label - + ); }; diff --git a/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js b/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js index 645ac0d259..0e1bd7250d 100644 --- a/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js +++ b/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js @@ -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( - - Promise.resolve(true)} - tagType={{ name: '', description: '', icon: '' }} - editMode={false} - submit={jest.fn()} - /> + + Promise.resolve(true)} + tagType={{ name: '', description: '', icon: '' }} + editMode={false} + submit={jest.fn()} + /> ) @@ -35,15 +40,15 @@ test('renders correctly for creating without permissions', () => { .create( - Promise.resolve(true)} - tagType={{ name: '', description: '', icon: '' }} - editMode={false} - submit={jest.fn()} - /> + Promise.resolve(true)} + tagType={{ name: '', description: '', icon: '' }} + editMode={false} + submit={jest.fn()} + /> ) @@ -55,16 +60,18 @@ test('it supports editMode', () => { const tree = renderer .create( - - Promise.resolve(true)} - tagType={{ name: '', description: '', icon: '' }} - editMode - submit={jest.fn()} - /> + + Promise.resolve(true)} + tagType={{ name: '', description: '', icon: '' }} + editMode + submit={jest.fn()} + /> ) diff --git a/frontend/src/component/tag-types/__tests__/tag-type-list-component-test.js b/frontend/src/component/tag-types/__tests__/tag-type-list-component-test.js index c9be42d784..ebdedf1750 100644 --- a/frontend/src/component/tag-types/__tests__/tag-type-list-component-test.js +++ b/frontend/src/component/tag-types/__tests__/tag-type-list-component-test.js @@ -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( - + { const tree = renderer.create( - + { +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 ( -
+
- Tag types allows you to group tags together in the management UI + Tag types allows you to group tags together in the + management UI - + - - - - } elseShow={You do not have permissions to save.} /> + + + + } + elseShow={ + You do not have permissions to save. + } + />
diff --git a/frontend/src/component/tags/TagList/TagList.jsx b/frontend/src/component/tags/TagList/TagList.jsx index 1ba550d9bf..c2e7e4d5e1 100644 --- a/frontend/src/component/tags/TagList/TagList.jsx +++ b/frontend/src/component/tags/TagList/TagList.jsx @@ -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 => ( - + label @@ -43,7 +55,9 @@ const TagList = ({ tags, fetchTags, removeTag }) => { const DeleteButton = ({ tagType, tagValue }) => ( - remove({ type: tagType, value: tagValue }, e)}> + remove({ type: tagType, value: tagValue }, e)} + > delete @@ -61,7 +75,10 @@ const TagList = ({ tags, fetchTags, removeTag }) => { history.push('/tags/create')}> + history.push('/tags/create')} + > add } @@ -82,7 +99,14 @@ const TagList = ({ tags, fetchTags, removeTag }) => { /> ); return ( - } />}> + } + /> + } + > 0} diff --git a/frontend/src/component/user/DemoAuth/DemoAuth.jsx b/frontend/src/component/user/DemoAuth/DemoAuth.jsx index b3b6170b55..136837c120 100644 --- a/frontend/src/component/user/DemoAuth/DemoAuth.jsx +++ b/frontend/src/component/user/DemoAuth/DemoAuth.jsx @@ -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 (
- Unleash Logo + Unleash Logo

Access the Unleash demo instance

No further data or Credit Card required

@@ -46,7 +41,6 @@ const DemoAuth = ({ type="email" />    -

- By accessing our demo instance, you agree to the Unleash  + By accessing our demo instance, you agree to the + Unleash  - Customer Terms of Service - and  - + target="_blank" + rel="noreferrer" + > + Customer Terms of Service + {' '} + and  + Privacy Policy -

@@ -77,7 +78,6 @@ const DemoAuth = ({ DemoAuth.propTypes = { authDetails: PropTypes.object.isRequired, demoLogin: PropTypes.func.isRequired, - loadInitialData: PropTypes.func.isRequired, history: PropTypes.object.isRequired, }; diff --git a/frontend/src/component/user/ForgottenPassword/ForgottenPassword.tsx b/frontend/src/component/user/ForgottenPassword/ForgottenPassword.tsx index d2027c58b4..f5951f511e 100644 --- a/frontend/src/component/user/ForgottenPassword/ForgottenPassword.tsx +++ b/frontend/src/component/user/ForgottenPassword/ForgottenPassword.tsx @@ -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', }, diff --git a/frontend/src/component/user/Login/Login.jsx b/frontend/src/component/user/Login/Login.jsx index 1a1dff1be9..2de8226c52 100644 --- a/frontend/src/component/user/Login/Login.jsx +++ b/frontend/src/component/user/Login/Login.jsx @@ -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'); diff --git a/frontend/src/component/user/Login/index.js b/frontend/src/component/user/Login/index.js index b86c0d8b2c..1e97513273 100644 --- a/frontend/src/component/user/Login/index.js +++ b/frontend/src/component/user/Login/index.js @@ -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); diff --git a/frontend/src/component/user/PasswordAuth/PasswordAuth.jsx b/frontend/src/component/user/PasswordAuth/PasswordAuth.jsx index d5be186716..902371622a 100644 --- a/frontend/src/component/user/PasswordAuth/PasswordAuth.jsx +++ b/frontend/src/component/user/PasswordAuth/PasswordAuth.jsx @@ -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, }; diff --git a/frontend/src/component/user/SimpleAuth/SimpleAuth.jsx b/frontend/src/component/user/SimpleAuth/SimpleAuth.jsx index c92dda4788..0efb6b5779 100644 --- a/frontend/src/component/user/SimpleAuth/SimpleAuth.jsx +++ b/frontend/src/component/user/SimpleAuth/SimpleAuth.jsx @@ -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, }; diff --git a/frontend/src/component/user/StandaloneBanner/StandaloneBanner.tsx b/frontend/src/component/user/StandaloneBanner/StandaloneBanner.tsx index 23a8bed3d4..e3fdc3b60e 100644 --- a/frontend/src/component/user/StandaloneBanner/StandaloneBanner.tsx +++ b/frontend/src/component/user/StandaloneBanner/StandaloneBanner.tsx @@ -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'; diff --git a/frontend/src/component/user/UserProfile/EditProfile/EditProfile.tsx b/frontend/src/component/user/UserProfile/EditProfile/EditProfile.tsx index 02937afc7c..6b9c0d6220 100644 --- a/frontend/src/component/user/UserProfile/EditProfile/EditProfile.tsx +++ b/frontend/src/component/user/UserProfile/EditProfile/EditProfile.tsx @@ -15,6 +15,7 @@ import { OK, UNAUTHORIZED, } from '../../../../constants/statusCodes'; +import { formatApiPath } from '../../../../utils/format-path'; interface IEditProfileProps { setEditingProfile: React.Dispatch>; @@ -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', diff --git a/frontend/src/component/user/UserProfile/UserProfile.jsx b/frontend/src/component/user/UserProfile/UserProfile.jsx index bd8a1867c2..790d98dce6 100644 --- a/frontend/src/component/user/UserProfile/UserProfile.jsx +++ b/frontend/src/component/user/UserProfile/UserProfile.jsx @@ -37,7 +37,6 @@ const UserProfile = ({ useEffect(() => { fetchUser(); - const locale = navigator.language || navigator.userLanguage; let found = possibleLocales.find(l => l.toLowerCase().includes(locale.toLowerCase()) diff --git a/frontend/src/component/user/UserProfile/UserProfileContent/UserProfileContent.jsx b/frontend/src/component/user/UserProfile/UserProfileContent/UserProfileContent.jsx index b22dc1cf43..6cda828e03 100644 --- a/frontend/src/component/user/UserProfile/UserProfileContent/UserProfileContent.jsx +++ b/frontend/src/component/user/UserProfile/UserProfileContent/UserProfileContent.jsx @@ -24,7 +24,6 @@ const UserProfileContent = ({ imageUrl, currentLocale, setCurrentLocale, - location, logoutUser, }) => { const commonStyles = useCommonStyles(); diff --git a/frontend/src/component/user/authentication-component.jsx b/frontend/src/component/user/authentication-component.jsx index 452069b7fe..53ec38c566 100644 --- a/frontend/src/component/user/authentication-component.jsx +++ b/frontend/src/component/user/authentication-component.jsx @@ -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 { ); @@ -40,7 +38,6 @@ class AuthComponent extends React.Component { ); @@ -49,7 +46,6 @@ class AuthComponent extends React.Component { ); diff --git a/frontend/src/component/user/authentication-container.jsx b/frontend/src/component/user/authentication-container.jsx index 0f9a2d8102..8a5556dc8b 100644 --- a/frontend/src/component/user/authentication-container.jsx +++ b/frontend/src/component/user/authentication-container.jsx @@ -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); diff --git a/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.tsx b/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.tsx index cf2da0be2b..f801d43780 100644 --- a/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.tsx +++ b/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.tsx @@ -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]); diff --git a/frontend/src/component/user/common/ResetPasswordForm/ResetPasswordForm.tsx b/frontend/src/component/user/common/ResetPasswordForm/ResetPasswordForm.tsx index 474e313fdc..ea8d660667 100644 --- a/frontend/src/component/user/common/ResetPasswordForm/ResetPasswordForm.tsx +++ b/frontend/src/component/user/common/ResetPasswordForm/ResetPasswordForm.tsx @@ -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', }, diff --git a/frontend/src/hooks/useAdminUsersApi.ts b/frontend/src/hooks/useAdminUsersApi.ts index cf181f2d23..898c6aa6f5 100644 --- a/frontend/src/hooks/useAdminUsersApi.ts +++ b/frontend/src/hooks/useAdminUsersApi.ts @@ -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 }), diff --git a/frontend/src/hooks/useResetPassword.ts b/frontend/src/hooks/useResetPassword.ts index 106b3d76af..159222da0a 100644 --- a/frontend/src/hooks/useResetPassword.ts +++ b/frontend/src/hooks/useResetPassword.ts @@ -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 = () => { diff --git a/frontend/src/hooks/useUsers.ts b/frontend/src/hooks/useUsers.ts index 5196858401..67530d84eb 100644 --- a/frontend/src/hooks/useUsers.ts +++ b/frontend/src/hooks/useUsers.ts @@ -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); diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index f20f133363..ecf5507432 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -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( - + @@ -52,7 +53,7 @@ ReactDOM.render( - + , diff --git a/frontend/src/page/admin/api/api-key-list.jsx b/frontend/src/page/admin/api/api-key-list.jsx index 6b786e61a5..cfdddcb831 100644 --- a/frontend/src/page/admin/api/api-key-list.jsx +++ b/frontend/src/page/admin/api/api-key-list.jsx @@ -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 (
- +

Read the{' '} - + Getting started guide {' '} - 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.


- API URL:
{unleashUrl}/api/
+ API URL: {' '} +
{unleashUrl}/api/
- -

- + +
+
+
@@ -58,10 +83,17 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, unleashUr {keys.map(item => ( - {formatFullDateTimeWithLocale(item.createdAt, location.locale)} + {formatFullDateTimeWithLocale( + item.createdAt, + location.locale + )} + + + {item.username} + + + {item.type} - {item.username} - {item.type} @@ -95,7 +127,10 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, unleashUr >
Are you sure you want to delete?
- } /> + } + /> ); } diff --git a/frontend/src/page/admin/auth/google-auth.jsx b/frontend/src/page/admin/auth/google-auth.jsx index 4d84dd5572..7773419909 100644 --- a/frontend/src/page/admin/auth/google-auth.jsx +++ b/frontend/src/page/admin/auth/google-auth.jsx @@ -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 }) Please read the{' '} - + documentation {' '} to learn how to integrate with Google OAuth 2.0.
- Callback URL: {unleashUrl}/auth/google/callback + Callback URL:{' '} + {unleashUrl}/auth/google/callback
@@ -77,12 +87,16 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl }) Enable

- 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.

- + {data.enabled ? 'Enabled' : 'Disabled'} @@ -90,7 +104,10 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl }) Client ID -

(Required) The Client ID provided by Google when registering the application.

+

+ (Required) The Client ID provided by Google when + registering the application. +

Client Secret -

(Required) Client Secret provided by Google when registering the application.

+

+ (Required) Client Secret provided by Google when + registering the application. +

Unleash hostname

- (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{' '} - https://[unleash.hostname.com]/auth/google/callback + + https://[unleash.hostname.com]/auth/google/callback +

@@ -150,10 +173,17 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl }) Auto-create users -

Enable automatic creation of new users when signing in with Google.

+

+ Enable automatic creation of new users when signing + in with Google. +

- + Auto-create users @@ -161,7 +191,10 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl }) Email domains -

(Optional) Comma separated list of email domains that should be allowed to sign in.

+

+ (Optional) Comma separated list of email domains + that should be allowed to sign in. +

- {' '} {info} diff --git a/frontend/src/page/admin/auth/saml-auth.jsx b/frontend/src/page/admin/auth/saml-auth.jsx index 7c635f4f91..04ef60ec44 100644 --- a/frontend/src/page/admin/auth/saml-auth.jsx +++ b/frontend/src/page/admin/auth/saml-auth.jsx @@ -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 You need to be a root admin to access this section.; + return ( + + You need to be a root admin to access this section. + + ); } const updateField = e => { @@ -65,11 +69,17 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) { Please read the{' '} - + documentation {' '} - to learn how to integrate with specific SAML 2.0 providers (Okta, Keycloak, etc).
- Callback URL: {unleashUrl}/auth/saml/callback + to learn how to integrate with specific SAML 2.0 + providers (Okta, Keycloak, etc).
+ Callback URL:{' '} + {unleashUrl}/auth/saml/callback
@@ -80,7 +90,12 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {

Enable SAML 2.0 Authentication.

- + {data.enabled ? 'Enabled' : 'Disabled'} @@ -105,7 +120,10 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) { Single Sign-On URL -

(Required) The url to redirect the user to for signing in.

+

+ (Required) The url to redirect the user to for + signing in. +

X.509 Certificate -

(Required) The certificate used to sign the SAML 2.0 request.

+

+ (Required) The certificate used to sign the SAML 2.0 + request. +

Auto-create users -

Enable automatic creation of new users when signing in with Saml.

+

+ Enable automatic creation of new users when signing + in with Saml. +

- + Auto-create users @@ -157,7 +185,10 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) { Email domains -

(Optional) Comma separated list of email domains that should be allowed to sign in.

+

+ (Optional) Comma separated list of email domains + that should be allowed to sign in. +

- {' '} {info} diff --git a/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserEmail/ConfirmUserEmail.tsx b/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserEmail/ConfirmUserEmail.tsx index 8deb364370..d2eefcbdec 100644 --- a/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserEmail/ConfirmUserEmail.tsx +++ b/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserEmail/ConfirmUserEmail.tsx @@ -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 { diff --git a/frontend/src/page/admin/users/UsersList/UsersList.jsx b/frontend/src/page/admin/users/UsersList/UsersList.jsx index 16e4490a20..21dcb8bdf9 100644 --- a/frontend/src/page/admin/users/UsersList/UsersList.jsx +++ b/frontend/src/page/admin/users/UsersList/UsersList.jsx @@ -251,4 +251,4 @@ UsersList.propTypes = { location: PropTypes.object.isRequired, }; -export default UsersList; \ No newline at end of file +export default UsersList; diff --git a/frontend/src/page/admin/users/index.js b/frontend/src/page/admin/users/index.js index b2c97ea039..7aca7ea94f 100644 --- a/frontend/src/page/admin/users/index.js +++ b/frontend/src/page/admin/users/index.js @@ -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 (
- } - elseShow={You need to be a root admin to access this section.} /> - + } + elseShow={ + + You need to be a root admin to access this section. + + } + />
); -} +}; UsersAdmin.propTypes = { match: PropTypes.object.isRequired, diff --git a/frontend/src/store/addons/api.js b/frontend/src/store/addons/api.js index 89958e3a51..72decf1494 100644 --- a/frontend/src/store/addons/api.js +++ b/frontend/src/store/addons/api.js @@ -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' }) diff --git a/frontend/src/store/application/api.js b/frontend/src/store/application/api.js index 8b799baab7..5a941d27f4 100644 --- a/frontend/src/store/application/api.js +++ b/frontend/src/store/application/api.js @@ -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' }) diff --git a/frontend/src/store/archive/api.js b/frontend/src/store/archive/api.js index 2741110e21..ea4661747f 100644 --- a/frontend/src/store/archive/api.js +++ b/frontend/src/store/archive/api.js @@ -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' }) diff --git a/frontend/src/store/context/actions.js b/frontend/src/store/context/actions.js index d2132d012a..93d4386c1a 100644 --- a/frontend/src/store/context/actions.js +++ b/frontend/src/store/context/actions.js @@ -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 }); diff --git a/frontend/src/store/context/api.js b/frontend/src/store/context/api.js index 2631b679ac..00423dbdd0 100644 --- a/frontend/src/store/context/api.js +++ b/frontend/src/store/context/api.js @@ -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' }) diff --git a/frontend/src/store/context/index.js b/frontend/src/store/context/index.js index 12898443b0..7274f98b24 100644 --- a/frontend/src/store/context/index.js +++ b/frontend/src/store/context/index.js @@ -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: diff --git a/frontend/src/store/e-api-admin/api.js b/frontend/src/store/e-api-admin/api.js index a4eca1f421..3c8195d190 100644 --- a/frontend/src/store/e-api-admin/api.js +++ b/frontend/src/store/e-api-admin/api.js @@ -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' }) diff --git a/frontend/src/store/feature-metrics/api.js b/frontend/src/store/feature-metrics/api.js index 69c187fac6..3fe10dd7ca 100644 --- a/frontend/src/store/feature-metrics/api.js +++ b/frontend/src/store/feature-metrics/api.js @@ -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' }) diff --git a/frontend/src/store/feature-tags/api.js b/frontend/src/store/feature-tags/api.js index e2145c3db7..5b0574408d 100644 --- a/frontend/src/store/feature-tags/api.js +++ b/frontend/src/store/feature-tags/api.js @@ -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) { diff --git a/frontend/src/store/feature-toggle/api.js b/frontend/src/store/feature-toggle/api.js index 4ba86e0b1f..6a24fbc183 100644 --- a/frontend/src/store/feature-toggle/api.js +++ b/frontend/src/store/feature-toggle/api.js @@ -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); diff --git a/frontend/src/store/feature-type/actions.js b/frontend/src/store/feature-type/actions.js index 57f020b830..7eed119f9f 100644 --- a/frontend/src/store/feature-type/actions.js +++ b/frontend/src/store/feature-type/actions.js @@ -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)); } diff --git a/frontend/src/store/feature-type/api.js b/frontend/src/store/feature-type/api.js index 22e39509fb..37074bfc1d 100644 --- a/frontend/src/store/feature-type/api.js +++ b/frontend/src/store/feature-type/api.js @@ -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' }) diff --git a/frontend/src/store/feature-type/index.js b/frontend/src/store/feature-type/index.js index 8bf89b52f6..f31efacd7a 100644 --- a/frontend/src/store/feature-type/index.js +++ b/frontend/src/store/feature-type/index.js @@ -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; } diff --git a/frontend/src/store/history/api.js b/frontend/src/store/history/api.js index a2b535908a..8dfabe6f06 100644 --- a/frontend/src/store/history/api.js +++ b/frontend/src/store/history/api.js @@ -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' }) diff --git a/frontend/src/store/loader.js b/frontend/src/store/loader.js deleted file mode 100644 index 9b73aa4927..0000000000 --- a/frontend/src/store/loader.js +++ /dev/null @@ -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); - }; -} diff --git a/frontend/src/store/project/actions.js b/frontend/src/store/project/actions.js index c03e6a01da..4517a9f61d 100644 --- a/frontend/src/store/project/actions.js +++ b/frontend/src/store/project/actions.js @@ -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() diff --git a/frontend/src/store/project/api.js b/frontend/src/store/project/api.js index 09f066c8ab..ec0aa6653f 100644 --- a/frontend/src/store/project/api.js +++ b/frontend/src/store/project/api.js @@ -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' }) diff --git a/frontend/src/store/strategy/actions.js b/frontend/src/store/strategy/actions.js index a3fd1705d3..a9699b3c16 100644 --- a/frontend/src/store/strategy/actions.js +++ b/frontend/src/store/strategy/actions.js @@ -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)); }; } diff --git a/frontend/src/store/strategy/api.js b/frontend/src/store/strategy/api.js index 2387fb550f..dccff368c6 100644 --- a/frontend/src/store/strategy/api.js +++ b/frontend/src/store/strategy/api.js @@ -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' }) diff --git a/frontend/src/store/tag-type/actions.js b/frontend/src/store/tag-type/actions.js index 57391f5f37..cde0ef325e 100644 --- a/frontend/src/store/tag-type/actions.js +++ b/frontend/src/store/tag-type/actions.js @@ -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)); }; } diff --git a/frontend/src/store/tag-type/api.js b/frontend/src/store/tag-type/api.js index 9f85a266a8..53f180ac9d 100644 --- a/frontend/src/store/tag-type/api.js +++ b/frontend/src/store/tag-type/api.js @@ -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; diff --git a/frontend/src/store/tag/api.js b/frontend/src/store/tag/api.js index 150d84d8f4..17461526be 100644 --- a/frontend/src/store/tag/api.js +++ b/frontend/src/store/tag/api.js @@ -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' }) diff --git a/frontend/src/store/ui-bootstrap/actions.js b/frontend/src/store/ui-bootstrap/actions.js new file mode 100644 index 0000000000..f61c237f1d --- /dev/null +++ b/frontend/src/store/ui-bootstrap/actions.js @@ -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)); +} diff --git a/frontend/src/store/ui-bootstrap/api.js b/frontend/src/store/ui-bootstrap/api.js new file mode 100644 index 0000000000..98e0207798 --- /dev/null +++ b/frontend/src/store/ui-bootstrap/api.js @@ -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, +}; diff --git a/frontend/src/store/ui-config/api.js b/frontend/src/store/ui-config/api.js index 153916f78c..c340ea414c 100644 --- a/frontend/src/store/ui-config/api.js +++ b/frontend/src/store/ui-config/api.js @@ -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' }) diff --git a/frontend/src/store/user/actions.js b/frontend/src/store/user/actions.js index 4285025ee4..49de02ce64 100644 --- a/frontend/src/store/user/actions.js +++ b/frontend/src/store/user/actions.js @@ -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); }; } diff --git a/frontend/src/store/user/api.js b/frontend/src/store/user/api.js index e14458015b..261c39cbff 100644 --- a/frontend/src/store/user/api.js +++ b/frontend/src/store/user/api.js @@ -1,51 +1,52 @@ -import { throwIfNotSuccess, headers } from "../api-helper"; +import { formatApiPath } from '../../utils/format-path'; +import { throwIfNotSuccess, headers } from '../api-helper'; -const URI = "api/admin/user"; +const URI = formatApiPath('api/admin/user'); function logoutUser() { - return fetch(`logout`, { - method: "GET", - credentials: "include", + return fetch(formatApiPath('logout'), { + method: 'GET', + credentials: 'include', }).then(throwIfNotSuccess); } function fetchUser() { - return fetch(URI, { credentials: "include" }) + return fetch(URI, { credentials: 'include' }) .then(throwIfNotSuccess) - .then((response) => response.json()); + .then(response => response.json()); } function insecureLogin(path, user) { return fetch(path, { - method: "POST", - credentials: "include", + method: 'POST', + credentials: 'include', headers, body: JSON.stringify(user), }) .then(throwIfNotSuccess) - .then((response) => response.json()); + .then(response => response.json()); } function demoLogin(path, user) { return fetch(path, { - method: "POST", - credentials: "include", + method: 'POST', + credentials: 'include', headers, body: JSON.stringify(user), }) .then(throwIfNotSuccess) - .then((response) => response.json()); + .then(response => response.json()); } function passwordLogin(path, data) { return fetch(path, { - method: "POST", - credentials: "include", + method: 'POST', + credentials: 'include', headers, body: JSON.stringify(data), }) .then(throwIfNotSuccess) - .then((response) => response.json()); + .then(response => response.json()); } const api = { diff --git a/frontend/src/utils/format-path.ts b/frontend/src/utils/format-path.ts new file mode 100644 index 0000000000..6c356fb1b1 --- /dev/null +++ b/frontend/src/utils/format-path.ts @@ -0,0 +1,51 @@ +export const getBasePathGenerator = () => { + let basePath: string | undefined; + const DEFAULT = '::baseUriPath::'; + + return () => { + if (process.env.NODE_ENV === 'development') { + return ''; + } + + if (basePath !== undefined) { + return basePath; + } + const baseUriPath = document.querySelector( + 'meta[name="baseUriPath"]' + ); + + if (baseUriPath?.content) { + basePath = baseUriPath?.content; + + if (basePath === DEFAULT) { + basePath = ''; + return ''; + } + + return basePath; + } + basePath = ''; + return basePath; + }; +}; + +export const getBasePath = getBasePathGenerator(); + +export const formatApiPath = (path: string) => { + const basePath = getBasePath(); + + if (basePath) { + return `${basePath}/${path}`; + } + return `/${path}`; +}; + +export const formatAssetPath = (path: string) => { + const basePath = getBasePath(); + + if (basePath) { + return `${basePath}/${path}`; + } + + return path; +};