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 ;
+ return (
+
+ );
case 'jira-comment':
- return ;
+ return (
+
+ );
case 'webhook':
- return ;
+ return (
+
+ );
case 'teams':
- return ;
+ return (
+
+ );
case 'datadog':
- return ;
+ return (
+
+ );
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)}
-
+
{
history.push(`/addons/create/${provider.name}`)}
+ onClick={() =>
+ history.push(`/addons/create/${provider.name}`)
+ }
title="Configure"
>
Configure
diff --git a/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap b/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap
index dd52e7af88..3d593e4683 100644
--- a/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap
+++ b/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap
@@ -494,7 +494,8 @@ exports[`renders correctly with permissions 1`] = `
>
123.123.123.123
- last seen at
+ last seen at
+
02/23/2017, 03:56:49 PM
diff --git a/frontend/src/component/application/__tests__/application-edit-component-test.js b/frontend/src/component/application/__tests__/application-edit-component-test.js
index 65499899ed..3a86caf41d 100644
--- a/frontend/src/component/application/__tests__/application-edit-component-test.js
+++ b/frontend/src/component/application/__tests__/application-edit-component-test.js
@@ -4,7 +4,12 @@ import { ThemeProvider } from '@material-ui/core';
import ClientApplications from '../application-edit-component';
import renderer from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom';
-import { ADMIN, CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../AccessProvider/permissions';
+import {
+ ADMIN,
+ CREATE_FEATURE,
+ CREATE_STRATEGY,
+ UPDATE_APPLICATION,
+} from '../../AccessProvider/permissions';
import theme from '../../../themes/main-theme';
import { createFakeStore } from '../../../accessStoreFake';
@@ -13,7 +18,7 @@ import AccessProvider from '../../AccessProvider/AccessProvider';
test('renders correctly if no application', () => {
const tree = renderer
.create(
-
+
Promise.resolve({})}
storeApplicationMetaData={jest.fn()}
@@ -32,51 +37,51 @@ test('renders correctly without permission', () => {
.create(
-
- Promise.resolve({})}
- storeApplicationMetaData={jest.fn()}
- deleteApplication={jest.fn()}
- history={{}}
- application={{
- appName: 'test-app',
- instances: [
- {
- instanceId: 'instance-1',
- clientIp: '123.123.123.123',
- lastSeen: '2017-02-23T15:56:49',
- sdkVersion: '4.0',
- },
- ],
- strategies: [
- {
- name: 'StrategyA',
- description: 'A description',
- },
- {
- name: 'StrategyB',
- description: 'B description',
- notFound: true,
- },
- ],
- seenToggles: [
- {
- name: 'ToggleA',
- description: 'this is A toggle',
- enabled: true,
- },
- {
- name: 'ToggleB',
- description: 'this is B toggle',
- enabled: false,
- notFound: true,
- },
- ],
- url: 'http://example.org',
- description: 'app description',
- }}
- location={{ locale: 'en-GB' }}
- />
+
+ Promise.resolve({})}
+ storeApplicationMetaData={jest.fn()}
+ deleteApplication={jest.fn()}
+ history={{}}
+ application={{
+ appName: 'test-app',
+ instances: [
+ {
+ instanceId: 'instance-1',
+ clientIp: '123.123.123.123',
+ lastSeen: '2017-02-23T15:56:49',
+ sdkVersion: '4.0',
+ },
+ ],
+ strategies: [
+ {
+ name: 'StrategyA',
+ description: 'A description',
+ },
+ {
+ name: 'StrategyB',
+ description: 'B description',
+ notFound: true,
+ },
+ ],
+ seenToggles: [
+ {
+ name: 'ToggleA',
+ description: 'this is A toggle',
+ enabled: true,
+ },
+ {
+ name: 'ToggleB',
+ description: 'this is B toggle',
+ enabled: false,
+ notFound: true,
+ },
+ ],
+ url: 'http://example.org',
+ description: 'app description',
+ }}
+ location={{ locale: 'en-GB' }}
+ />
@@ -91,51 +96,53 @@ test('renders correctly with permissions', () => {
.create(
-
- Promise.resolve({})}
- storeApplicationMetaData={jest.fn()}
- history={{}}
- deleteApplication={jest.fn()}
- application={{
- appName: 'test-app',
- instances: [
- {
- instanceId: 'instance-1',
- clientIp: '123.123.123.123',
- lastSeen: '2017-02-23T15:56:49',
- sdkVersion: '4.0',
- },
- ],
- strategies: [
- {
- name: 'StrategyA',
- description: 'A description',
- },
- {
- name: 'StrategyB',
- description: 'B description',
- notFound: true,
- },
- ],
- seenToggles: [
- {
- name: 'ToggleA',
- description: 'this is A toggle',
- enabled: true,
- },
- {
- name: 'ToggleB',
- description: 'this is B toggle',
- enabled: false,
- notFound: true,
- },
- ],
- url: 'http://example.org',
- description: 'app description',
- }}
- location={{ locale: 'en-GB' }}
- />
+
+ Promise.resolve({})}
+ storeApplicationMetaData={jest.fn()}
+ history={{}}
+ deleteApplication={jest.fn()}
+ application={{
+ appName: 'test-app',
+ instances: [
+ {
+ instanceId: 'instance-1',
+ clientIp: '123.123.123.123',
+ lastSeen: '2017-02-23T15:56:49',
+ sdkVersion: '4.0',
+ },
+ ],
+ strategies: [
+ {
+ name: 'StrategyA',
+ description: 'A description',
+ },
+ {
+ name: 'StrategyB',
+ description: 'B description',
+ notFound: true,
+ },
+ ],
+ seenToggles: [
+ {
+ name: 'ToggleA',
+ description: 'this is A toggle',
+ enabled: true,
+ },
+ {
+ name: 'ToggleB',
+ description: 'this is B toggle',
+ enabled: false,
+ notFound: true,
+ },
+ ],
+ url: 'http://example.org',
+ description: 'app description',
+ }}
+ location={{ locale: 'en-GB' }}
+ />
diff --git a/frontend/src/component/application/application-edit-component.js b/frontend/src/component/application/application-edit-component.js
index bbfb73bf4d..ea4e993a14 100644
--- a/frontend/src/component/application/application-edit-component.js
+++ b/frontend/src/component/application/application-edit-component.js
@@ -2,9 +2,20 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
-import { Avatar, Link, Icon, IconButton, Button, LinearProgress, Typography } from '@material-ui/core';
+import {
+ Avatar,
+ Link,
+ Icon,
+ IconButton,
+ Button,
+ LinearProgress,
+ Typography,
+} from '@material-ui/core';
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
-import { formatFullDateTimeWithLocale, formatDateWithLocale } from '../common/util';
+import {
+ formatFullDateTimeWithLocale,
+ formatDateWithLocale,
+} from '../common/util';
import { UPDATE_APPLICATION } from '../AccessProvider/permissions';
import ApplicationView from './application-view';
import ApplicationUpdate from './application-update';
@@ -37,9 +48,12 @@ class ClientApplications extends PureComponent {
}
componentDidMount() {
- this.props.fetchApplication(this.props.appName).finally(() => this.setState({ loading: false }));
+ this.props
+ .fetchApplication(this.props.appName)
+ .finally(() => this.setState({ loading: false }));
}
- formatFullDateTime = v => formatFullDateTimeWithLocale(v, this.props.location.locale);
+ formatFullDateTime = v =>
+ formatFullDateTimeWithLocale(v, this.props.location.locale);
formatDate = v => formatDateWithLocale(v, this.props.location.locale);
deleteApplication = async evt => {
@@ -64,7 +78,16 @@ class ClientApplications extends PureComponent {
}
const { hasAccess } = this.context;
const { application, storeApplicationMetaData } = this.props;
- const { appName, instances, strategies, seenToggles, url, description, icon = 'apps', createdAt } = application;
+ const {
+ appName,
+ instances,
+ strategies,
+ seenToggles,
+ url,
+ description,
+ icon = 'apps',
+ createdAt,
+ } = application;
const toggleModal = () => {
this.setState(prev => ({ ...prev, prompt: !prev.prompt }));
@@ -95,7 +118,10 @@ class ClientApplications extends PureComponent {
{
label: 'Edit application',
component: (
-
+
),
},
];
@@ -131,7 +157,11 @@ class ClientApplications extends PureComponent {
+
Delete
}
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/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 }) => {
history.push('/tag-types/create')}
+ onClick={() =>
+ history.push('/tag-types/create')
+ }
>
Add new tag type
@@ -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
-
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 (
@@ -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.
+
-
+
Save
{' '}
{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.
+
-
+
Save
{' '}
{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;
+};