diff --git a/frontend/src/component/admin/auth/authentication.jsx b/frontend/src/component/admin/auth/AuthSettings.tsx
similarity index 82%
rename from frontend/src/component/admin/auth/authentication.jsx
rename to frontend/src/component/admin/auth/AuthSettings.tsx
index ea7ea4f5e6..616e95330f 100644
--- a/frontend/src/component/admin/auth/authentication.jsx
+++ b/frontend/src/component/admin/auth/AuthSettings.tsx
@@ -1,16 +1,18 @@
import React from 'react';
-import PropTypes from 'prop-types';
import AdminMenu from '../menu/AdminMenu';
import { Alert } from '@material-ui/lab';
-import GoogleAuth from './google-auth-container';
-import SamlAuth from './saml-auth-container';
-import OidcAuth from './oidc-auth-container';
-import PasswordAuthSettings from './PasswordAuthSettings';
import TabNav from '../../common/TabNav/TabNav';
import PageContent from '../../common/PageContent/PageContent';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
+import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
+import { OidcAuth } from './OidcAuth/OidcAuth';
+import { SamlAuth } from './SamlAuth/SamlAuth';
+import { PasswordAuth } from './PasswordAuth/PasswordAuth';
+import { GoogleAuth } from './GoogleAuth/GoogleAuth';
+
+export const AuthSettings = () => {
+ const { authenticationType } = useUiConfig().uiConfig;
-function AdminAuthPage({ authenticationType, history }) {
const tabs = [
{
label: 'OpenID Connect',
@@ -22,7 +24,7 @@ function AdminAuthPage({ authenticationType, history }) {
},
{
label: 'Password',
- component: ,
+ component: ,
},
{
label: 'Google',
@@ -32,7 +34,7 @@ function AdminAuthPage({ authenticationType, history }) {
return (
);
-}
-
-AdminAuthPage.propTypes = {
- match: PropTypes.object.isRequired,
- history: PropTypes.object.isRequired,
- authenticationType: PropTypes.string,
};
-
-export default AdminAuthPage;
diff --git a/frontend/src/component/admin/auth/AutoCreateForm/AutoCreateForm.tsx b/frontend/src/component/admin/auth/AutoCreateForm/AutoCreateForm.tsx
index 217bae981f..373cf547ae 100644
--- a/frontend/src/component/admin/auth/AutoCreateForm/AutoCreateForm.tsx
+++ b/frontend/src/component/admin/auth/AutoCreateForm/AutoCreateForm.tsx
@@ -1,102 +1,119 @@
import React, { ChangeEvent, Fragment } from 'react';
-import { FormControl, Grid, MenuItem, Switch, TextField, Select, InputLabel, FormControlLabel } from '@material-ui/core';
+import {
+ FormControl,
+ FormControlLabel,
+ Grid,
+ InputLabel,
+ MenuItem,
+ Select,
+ Switch,
+ TextField,
+} from '@material-ui/core';
-interface Props {
+interface Props {
data?: {
enabled: boolean;
autoCreate: boolean;
defaultRootRole?: string;
emailDomains?: string;
-
};
setValue: (name: string, value: string | boolean) => void;
}
-function AutoCreateForm({ data = { enabled: false, autoCreate: false }, setValue }: Props) {
+export const AutoCreateForm = ({
+ data = { enabled: false, autoCreate: false },
+ setValue,
+}: Props) => {
const updateAutoCreate = () => {
setValue('autoCreate', !data.autoCreate);
- }
+ };
- const updateDefaultRootRole = (evt: ChangeEvent<{ name?: string; value: unknown }>) => {
+ const updateDefaultRootRole = (
+ evt: ChangeEvent<{ name?: string; value: unknown }>
+ ) => {
setValue('defaultRootRole', evt.target.value as string);
- }
+ };
const updateField = (e: ChangeEvent) => {
setValue(e.target.name, e.target.value);
- }
+ };
-return (
-
-
-
- Auto-create users
-
- Enable automatic creation of new users when signing in.
-
+ return (
+
+
+
+ Auto-create users
+
+ Enable automatic creation of new users when signing in.
+
+
+
+
+ }
+ label="Auto-create users"
+ />
+
-
-
- }
- label="Auto-create users"
- />
+
+
+ Default Root Role
+
+ Choose which root role the user should get when no
+ explicit role mapping exists.
+
+
+
+
+
+ Default Role
+
+
+ {/*consider these from API or constants. */}
+ Viewer
+ Editor
+ Admin
+
+
+
-
-
-
- Default Root Role
-
- Choose which root role the user should get when no explicit role mapping exists.
-
+
+
+ Email domains
+
+ Comma separated list of email domains that should be
+ allowed to sign in.
+
+
+
+
+
-
-
- Default Role
-
- {/*consider these from API or constants. */}
- Viewer
- Editor
- Admin
-
-
-
-
-
-
- Email domains
-
- Comma separated list of email domains
- that should be allowed to sign in.
-
-
-
-
-
-
- );
-}
-export default AutoCreateForm;
+
+ );
+};
diff --git a/frontend/src/component/admin/auth/google-auth.jsx b/frontend/src/component/admin/auth/GoogleAuth/GoogleAuth.tsx
similarity index 78%
rename from frontend/src/component/admin/auth/google-auth.jsx
rename to frontend/src/component/admin/auth/GoogleAuth/GoogleAuth.tsx
index 52342bd4da..f9860a1fe0 100644
--- a/frontend/src/component/admin/auth/google-auth.jsx
+++ b/frontend/src/component/admin/auth/GoogleAuth/GoogleAuth.tsx
@@ -1,5 +1,4 @@
-import React, { useState, useEffect, useContext } from 'react';
-import PropTypes from 'prop-types';
+import React, { useContext, useEffect, useState } from 'react';
import {
Button,
FormControlLabel,
@@ -8,30 +7,32 @@ import {
TextField,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
-import PageContent from '../../common/PageContent/PageContent';
-import AccessContext from '../../../contexts/AccessContext';
-import { ADMIN } from '../../providers/AccessProvider/permissions';
+import PageContent from '../../../common/PageContent/PageContent';
+import AccessContext from '../../../../contexts/AccessContext';
+import { ADMIN } from '../../../providers/AccessProvider/permissions';
+import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
+import useAuthSettings from '../../../../hooks/api/getters/useAuthSettings/useAuthSettings';
+import useAuthSettingsApi from '../../../../hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
+import useToast from '../../../../hooks/useToast';
+import { formatUnknownError } from '../../../../utils/format-unknown-error';
+import { removeEmptyStringFields } from '../../../../utils/remove-empty-string-fields';
const initialState = {
enabled: false,
autoCreate: false,
unleashHostname: location.hostname,
+ clientId: '',
+ clientSecret: '',
+ emailDomains: '',
};
-function GoogleAuth({
- config,
- getGoogleConfig,
- updateGoogleConfig,
- unleashUrl,
-}) {
+export const GoogleAuth = () => {
+ const { setToastData, setToastApiError } = useToast();
+ const { uiConfig } = useUiConfig();
const [data, setData] = useState(initialState);
- const [info, setInfo] = useState();
const { hasAccess } = useContext(AccessContext);
-
- useEffect(() => {
- getGoogleConfig();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ const { config } = useAuthSettings('google');
+ const { updateSettings, errors, loading } = useAuthSettingsApi('google');
useEffect(() => {
if (config.clientId) {
@@ -43,10 +44,10 @@ function GoogleAuth({
return You need admin privileges to access this section. ;
}
- const updateField = e => {
+ const updateField = (event: React.ChangeEvent) => {
setData({
...data,
- [e.target.name]: e.target.value,
+ [event.target.name]: event.target.value,
});
};
@@ -58,19 +59,22 @@ function GoogleAuth({
setData({ ...data, autoCreate: !data.autoCreate });
};
- const onSubmit = async e => {
- e.preventDefault();
- setInfo('...saving');
+ const onSubmit = async (event: React.SyntheticEvent) => {
+ event.preventDefault();
+
try {
- await updateGoogleConfig(data);
- setInfo('Settings stored');
- setTimeout(() => setInfo(''), 2000);
- } catch (e) {
- setInfo(e.message);
+ await updateSettings(removeEmptyStringFields(data));
+ setToastData({
+ title: 'Settings stored',
+ type: 'success',
+ });
+ } catch (err) {
+ setToastApiError(formatUnknownError(err));
}
};
+
return (
-
+
@@ -84,7 +88,7 @@ function GoogleAuth({
{' '}
to learn how to integrate with Google OAuth 2.0.
Callback URL:{' '}
- {unleashUrl}/auth/google/callback
+ {uiConfig.unleashUrl}/auth/google/callback
@@ -125,7 +129,7 @@ function GoogleAuth({
label="Client ID"
name="clientId"
placeholder=""
- value={data.clientId || ''}
+ value={data.clientId}
style={{ width: '400px' }}
variant="outlined"
size="small"
@@ -146,7 +150,7 @@ function GoogleAuth({
onChange={updateField}
label="Client Secret"
name="clientSecret"
- value={data.clientSecret || ''}
+ value={data.clientSecret}
placeholder=""
style={{ width: '400px' }}
variant="outlined"
@@ -195,9 +199,7 @@ function GoogleAuth({
onChange={updateAutoCreate}
name="enabled"
checked={data.autoCreate}
- >
- Auto-create users
-
+ />
@@ -229,22 +231,18 @@ function GoogleAuth({
variant="contained"
color="primary"
type="submit"
+ disabled={loading}
>
Save
{' '}
- {info}
+
+
+ {errors?.message}
+
+
);
-}
-
-GoogleAuth.propTypes = {
- config: PropTypes.object,
- unleashUrl: PropTypes.string,
- getGoogleConfig: PropTypes.func.isRequired,
- updateGoogleConfig: PropTypes.func.isRequired,
};
-
-export default GoogleAuth;
diff --git a/frontend/src/component/admin/auth/oidc-auth.jsx b/frontend/src/component/admin/auth/OidcAuth/OidcAuth.tsx
similarity index 70%
rename from frontend/src/component/admin/auth/oidc-auth.jsx
rename to frontend/src/component/admin/auth/OidcAuth/OidcAuth.tsx
index 306495df17..c32b10da00 100644
--- a/frontend/src/component/admin/auth/oidc-auth.jsx
+++ b/frontend/src/component/admin/auth/OidcAuth/OidcAuth.tsx
@@ -1,5 +1,4 @@
-import React, { useState, useEffect, useContext } from 'react';
-import PropTypes from 'prop-types';
+import React, { useContext, useEffect, useState } from 'react';
import {
Button,
FormControlLabel,
@@ -8,34 +7,40 @@ import {
TextField,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
-import PageContent from '../../common/PageContent/PageContent';
-import AccessContext from '../../../contexts/AccessContext';
-import { ADMIN } from '../../providers/AccessProvider/permissions';
-import AutoCreateForm from './AutoCreateForm/AutoCreateForm';
+import PageContent from '../../../common/PageContent/PageContent';
+import AccessContext from '../../../../contexts/AccessContext';
+import { ADMIN } from '../../../providers/AccessProvider/permissions';
+import { AutoCreateForm } from '../AutoCreateForm/AutoCreateForm';
+import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
+import useAuthSettingsApi from '../../../../hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
+import useAuthSettings from '../../../../hooks/api/getters/useAuthSettings/useAuthSettings';
+import useToast from '../../../../hooks/useToast';
+import { formatUnknownError } from '../../../../utils/format-unknown-error';
+import { removeEmptyStringFields } from '../../../../utils/remove-empty-string-fields';
const initialState = {
enabled: false,
enableSingleSignOut: false,
autoCreate: false,
unleashHostname: location.hostname,
+ clientId: '',
+ discoverUrl: '',
+ secret: '',
+ acrValues: '',
};
-function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
+export const OidcAuth = () => {
+ const { setToastData, setToastApiError } = useToast();
+ const { uiConfig } = useUiConfig();
const [data, setData] = useState(initialState);
- const [info, setInfo] = useState();
- const [error, setError] = useState();
const { hasAccess } = useContext(AccessContext);
-
- useEffect(() => {
- getOidcConfig();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ const { config } = useAuthSettings('oidc');
+ const { updateSettings, errors, loading } = useAuthSettingsApi('oidc');
useEffect(() => {
if (config.discoverUrl) {
setData(config);
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]);
if (!hasAccess(ADMIN)) {
@@ -46,8 +51,8 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
);
}
- const updateField = e => {
- setValue(e.target.name, e.target.value);
+ const updateField = (event: React.ChangeEvent) => {
+ setValue(event.target.name, event.target.value);
};
const updateEnabled = () => {
@@ -58,28 +63,29 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
setData({ ...data, enableSingleSignOut: !data.enableSingleSignOut });
};
- const setValue = (field, value) => {
+ const setValue = (name: string, value: string | boolean) => {
setData({
...data,
- [field]: value,
+ [name]: value,
});
};
- const onSubmit = async e => {
- e.preventDefault();
- setInfo('...saving');
- setError('');
+ const onSubmit = async (event: React.SyntheticEvent) => {
+ event.preventDefault();
+
try {
- await updateOidcConfig(data);
- setInfo('Settings stored');
- setTimeout(() => setInfo(''), 2000);
- } catch (e) {
- setInfo('');
- setError(e.message);
+ await updateSettings(removeEmptyStringFields(data));
+ setToastData({
+ title: 'Settings stored',
+ type: 'success',
+ });
+ } catch (err) {
+ setToastApiError(formatUnknownError(err));
}
};
+
return (
-
+
@@ -94,7 +100,7 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
to learn how to integrate with specific Open Id Connect
providers (Okta, Keycloak, Google, etc).
Callback URL:{' '}
- {unleashUrl}/auth/oidc/callback
+ {uiConfig.unleashUrl}/auth/oidc/callback
@@ -128,7 +134,7 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
onChange={updateField}
label="Discover URL"
name="discoverUrl"
- value={data.discoverUrl || ''}
+ value={data.discoverUrl}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
@@ -146,7 +152,7 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
onChange={updateField}
label="Client ID"
name="clientId"
- value={data.clientId || ''}
+ value={data.clientId}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
@@ -167,7 +173,7 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
onChange={updateField}
label="Client Secret"
name="secret"
- value={data.secret || ''}
+ value={data.secret}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
@@ -180,7 +186,10 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
Enable Single Sign-Out
- If you enable Single Sign-Out Unleash will redirect the user to the IDP as part of the Sign-out process.
+
+ If you enable Single Sign-Out Unleash will redirect
+ the user to the IDP as part of the Sign-out process.
+
ACR Values
- Requested Authentication Context Class Reference values. If multiple values are specified they should be "space" separated. Will be sent as "acr_values" as
- part of the authentication request. Unleash will validate the acr value in the id token claims against the list of acr values.
+
+ Requested Authentication Context Class Reference
+ values. If multiple values are specified they should
+ be "space" separated. Will be sent as "acr_values"
+ as part of the authentication request. Unleash will
+ validate the acr value in the id token claims
+ against the list of acr values.
+
Save
{' '}
- {info}
- {error}
+
+
+ {errors?.message}
+
+
);
-}
-
-OidcAuth.propTypes = {
- config: PropTypes.object,
- unleash: PropTypes.string,
- getOidcConfig: PropTypes.func.isRequired,
- updateOidcConfig: PropTypes.func.isRequired,
};
-
-export default OidcAuth;
diff --git a/frontend/src/component/admin/auth/PasswordAuthSettings.tsx b/frontend/src/component/admin/auth/PasswordAuth/PasswordAuth.tsx
similarity index 55%
rename from frontend/src/component/admin/auth/PasswordAuthSettings.tsx
rename to frontend/src/component/admin/auth/PasswordAuth/PasswordAuth.tsx
index 16a7f46054..068563b2e6 100644
--- a/frontend/src/component/admin/auth/PasswordAuthSettings.tsx
+++ b/frontend/src/component/admin/auth/PasswordAuth/PasswordAuth.tsx
@@ -1,30 +1,28 @@
-import React, { useState, useContext, useEffect } from 'react';
-import {
- Button,
- FormControlLabel,
- Grid,
- Switch,
-} from '@material-ui/core';
+import React, { useContext, useEffect, useState } from 'react';
+import { Button, FormControlLabel, Grid, Switch } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
-import PageContent from '../../common/PageContent/PageContent';
-import AccessContext from '../../../contexts/AccessContext';
-import { ADMIN } from '../../providers/AccessProvider/permissions';
-import useAuthSettings from '../../../hooks/api/getters/useAuthSettings/useAuthSettings';
-import useAuthSettingsApi, {ISimpleAuthSettings } from '../../../hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
-import useToast from '../../../hooks/useToast';
+import PageContent from '../../../common/PageContent/PageContent';
+import AccessContext from '../../../../contexts/AccessContext';
+import { ADMIN } from '../../../providers/AccessProvider/permissions';
+import useAuthSettings from '../../../../hooks/api/getters/useAuthSettings/useAuthSettings';
+import useAuthSettingsApi, {
+ ISimpleAuthSettings,
+} from '../../../../hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
+import useToast from '../../../../hooks/useToast';
+import { formatUnknownError } from '../../../../utils/format-unknown-error';
-const PasswordAuthSettings = () => {
-
- const { setToastData } = useToast();
+export const PasswordAuth = () => {
+ const { setToastData, setToastApiError } = useToast();
const { config } = useAuthSettings('simple');
- const [disablePasswordAuth, setDisablePasswordAuth] = useState(false);
- const { updateSettings, errors, loading } = useAuthSettingsApi('simple')
+ const [disablePasswordAuth, setDisablePasswordAuth] =
+ useState(false);
+ const { updateSettings, errors, loading } =
+ useAuthSettingsApi('simple');
const { hasAccess } = useContext(AccessContext);
-
useEffect(() => {
setDisablePasswordAuth(!!config.disabled);
- }, [ config.disabled ]);
+ }, [config.disabled]);
if (!hasAccess(ADMIN)) {
return (
@@ -38,12 +36,13 @@ const PasswordAuthSettings = () => {
setDisablePasswordAuth(!disablePasswordAuth);
};
-
- const onSubmit = async evt => {
- evt.preventDefault();
+ const onSubmit = async (event: React.SyntheticEvent) => {
+ event.preventDefault();
try {
- const settings: ISimpleAuthSettings = { disabled: disablePasswordAuth };
+ const settings: ISimpleAuthSettings = {
+ disabled: disablePasswordAuth,
+ };
await updateSettings(settings);
setToastData({
title: 'Successfully saved',
@@ -52,20 +51,13 @@ const PasswordAuthSettings = () => {
type: 'success',
show: true,
});
- } catch (err: any) {
- setToastData({
- title: 'Could not store settings',
- text: err?.message,
- autoHideDuration: 4000,
- type: 'error',
- show: true,
- });
- setDisablePasswordAuth(config.disabled)
+ } catch (err) {
+ setToastApiError(formatUnknownError(err));
+ setDisablePasswordAuth(config.disabled);
}
-
};
return (
-
+
);
-}
-
-export default PasswordAuthSettings;
+};
diff --git a/frontend/src/component/admin/auth/saml-auth.jsx b/frontend/src/component/admin/auth/SamlAuth/SamlAuth.tsx
similarity index 77%
rename from frontend/src/component/admin/auth/saml-auth.jsx
rename to frontend/src/component/admin/auth/SamlAuth/SamlAuth.tsx
index 521500deec..26f8c7a293 100644
--- a/frontend/src/component/admin/auth/saml-auth.jsx
+++ b/frontend/src/component/admin/auth/SamlAuth/SamlAuth.tsx
@@ -1,5 +1,4 @@
-import React, { useState, useEffect, useContext } from 'react';
-import PropTypes from 'prop-types';
+import React, { useContext, useEffect, useState } from 'react';
import {
Button,
FormControlLabel,
@@ -8,32 +7,40 @@ import {
TextField,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
-import PageContent from '../../common/PageContent/PageContent';
-import AccessContext from '../../../contexts/AccessContext';
-import { ADMIN } from '../../providers/AccessProvider/permissions';
-import AutoCreateForm from './AutoCreateForm/AutoCreateForm';
+import PageContent from '../../../common/PageContent/PageContent';
+import AccessContext from '../../../../contexts/AccessContext';
+import { ADMIN } from '../../../providers/AccessProvider/permissions';
+import { AutoCreateForm } from '../AutoCreateForm/AutoCreateForm';
+import useToast from '../../../../hooks/useToast';
+import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
+import useAuthSettings from '../../../../hooks/api/getters/useAuthSettings/useAuthSettings';
+import useAuthSettingsApi from '../../../../hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
+import { formatUnknownError } from '../../../../utils/format-unknown-error';
+import { removeEmptyStringFields } from '../../../../utils/remove-empty-string-fields';
const initialState = {
enabled: false,
autoCreate: false,
unleashHostname: location.hostname,
+ entityId: '',
+ signOnUrl: '',
+ certificate: '',
+ signOutUrl: '',
+ spCertificate: '',
};
-function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
+export const SamlAuth = () => {
+ const { setToastData, setToastApiError } = useToast();
+ const { uiConfig } = useUiConfig();
const [data, setData] = useState(initialState);
- const [info, setInfo] = useState();
const { hasAccess } = useContext(AccessContext);
-
- useEffect(() => {
- getSamlConfig();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ const { config } = useAuthSettings('saml');
+ const { updateSettings, errors, loading } = useAuthSettingsApi('saml');
useEffect(() => {
if (config.entityId) {
setData(config);
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]);
if (!hasAccess(ADMIN)) {
@@ -44,34 +51,37 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
);
}
- const updateField = e => {
- setValue(e.target.name, e.target.value);
+ const updateField = (event: React.ChangeEvent) => {
+ setValue(event.target.name, event.target.value);
};
const updateEnabled = () => {
setData({ ...data, enabled: !data.enabled });
};
- const setValue = (field, value) => {
+ const setValue = (name: string, value: string | boolean) => {
setData({
...data,
- [field]: value,
+ [name]: value,
});
};
- const onSubmit = async e => {
- e.preventDefault();
- setInfo('...saving');
+ const onSubmit = async (event: React.SyntheticEvent) => {
+ event.preventDefault();
+
try {
- await updateSamlConfig(data);
- setInfo('Settings stored');
- setTimeout(() => setInfo(''), 2000);
- } catch (e) {
- setInfo(e.message);
+ await updateSettings(removeEmptyStringFields(data));
+ setToastData({
+ title: 'Settings stored',
+ type: 'success',
+ });
+ } catch (err) {
+ setToastApiError(formatUnknownError(err));
}
};
+
return (
-
+
@@ -86,7 +96,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
to learn how to integrate with specific SAML 2.0
providers (Okta, Keycloak, etc).
Callback URL:{' '}
- {unleashUrl}/auth/saml/callback
+ {uiConfig.unleashUrl}/auth/saml/callback
@@ -120,7 +130,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
onChange={updateField}
label="Entity ID"
name="entityId"
- value={data.entityId || ''}
+ value={data.entityId}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
@@ -142,7 +152,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
onChange={updateField}
label="Single Sign-On URL"
name="signOnUrl"
- value={data.signOnUrl || ''}
+ value={data.signOnUrl}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
@@ -164,7 +174,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
onChange={updateField}
label="X.509 Certificate"
name="certificate"
- value={data.certificate || ''}
+ value={data.certificate}
disabled={!data.enabled}
style={{ width: '100%' }}
InputProps={{
@@ -196,7 +206,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
onChange={updateField}
label="Single Sign-out URL"
name="signOutUrl"
- value={data.signOutUrl || ''}
+ value={data.signOutUrl}
disabled={!data.enabled}
style={{ width: '400px' }}
variant="outlined"
@@ -219,7 +229,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
onChange={updateField}
label="X.509 Certificate"
name="spCertificate"
- value={data.spCertificate || ''}
+ value={data.spCertificate}
disabled={!data.enabled}
style={{ width: '100%' }}
InputProps={{
@@ -243,22 +253,18 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
variant="contained"
color="primary"
type="submit"
+ disabled={loading}
>
Save
{' '}
- {info}
+
+
+ {errors?.message}
+
+
);
-}
-
-SamlAuth.propTypes = {
- config: PropTypes.object,
- unleash: PropTypes.string,
- getSamlConfig: PropTypes.func.isRequired,
- updateSamlConfig: PropTypes.func.isRequired,
};
-
-export default SamlAuth;
diff --git a/frontend/src/component/admin/auth/google-auth-container.js b/frontend/src/component/admin/auth/google-auth-container.js
deleted file mode 100644
index 9cc583aa52..0000000000
--- a/frontend/src/component/admin/auth/google-auth-container.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { connect } from 'react-redux';
-import GoogleAuth from './google-auth';
-import { getGoogleConfig, updateGoogleConfig } from '../../../store/e-admin-auth/actions';
-
-const mapStateToProps = state => ({
- config: state.authAdmin.get('google'),
- unleashUrl: state.uiConfig.toJS().unleashUrl,
-});
-
-const Container = connect(mapStateToProps, { getGoogleConfig, updateGoogleConfig })(GoogleAuth);
-
-export default Container;
diff --git a/frontend/src/component/admin/auth/index.js b/frontend/src/component/admin/auth/index.js
deleted file mode 100644
index 07d89be806..0000000000
--- a/frontend/src/component/admin/auth/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { connect } from 'react-redux';
-import component from './authentication';
-
-const mapStateToProps = state => ({
- authenticationType: state.uiConfig.toJS().authenticationType,
-});
-
-const Container = connect(mapStateToProps, { })(component);
-
-export default Container;
diff --git a/frontend/src/component/admin/auth/oidc-auth-container.js b/frontend/src/component/admin/auth/oidc-auth-container.js
deleted file mode 100644
index 903a76a203..0000000000
--- a/frontend/src/component/admin/auth/oidc-auth-container.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { connect } from 'react-redux';
-import OidcAuth from './oidc-auth';
-import { getOidcConfig, updateOidcConfig } from '../../../store/e-admin-auth/actions';
-
-const mapStateToProps = state => ({
- config: state.authAdmin.get('oidc'),
- unleashUrl: state.uiConfig.toJS().unleashUrl,
-});
-
-const OidcContainer = connect(mapStateToProps, { getOidcConfig, updateOidcConfig })(OidcAuth);
-
-export default OidcContainer;
diff --git a/frontend/src/component/admin/auth/saml-auth-container.js b/frontend/src/component/admin/auth/saml-auth-container.js
deleted file mode 100644
index 1d1fe9ac3a..0000000000
--- a/frontend/src/component/admin/auth/saml-auth-container.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { connect } from 'react-redux';
-import SamlAuth from './saml-auth';
-import { getSamlConfig, updateSamlConfig } from '../../../store/e-admin-auth/actions';
-
-const mapStateToProps = state => ({
- config: state.authAdmin.get('saml'),
- unleashUrl: state.uiConfig.toJS().unleashUrl,
-});
-
-const Container = connect(mapStateToProps, { getSamlConfig, updateSamlConfig })(SamlAuth);
-
-export default Container;
diff --git a/frontend/src/component/admin/menu/AdminMenu.tsx b/frontend/src/component/admin/menu/AdminMenu.tsx
index 1125b3e27f..1866957dac 100644
--- a/frontend/src/component/admin/menu/AdminMenu.tsx
+++ b/frontend/src/component/admin/menu/AdminMenu.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { NavLink } from 'react-router-dom';
-import { Paper, Tabs, Tab } from '@material-ui/core';
+import { NavLink, useLocation } from 'react-router-dom';
+import { Paper, Tab, Tabs } from '@material-ui/core';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
const navLinkStyle = {
@@ -13,18 +13,17 @@ const navLinkStyle = {
padding: '0.8rem 1.5rem',
};
-const activeNavLinkStyle = {
+const activeNavLinkStyle: React.CSSProperties = {
fontWeight: 'bold',
borderRadius: '3px',
padding: '0.8rem 1.5rem',
};
-function AdminMenu({ history }) {
+function AdminMenu() {
const { uiConfig } = useUiConfig();
+ const { pathname } = useLocation();
const { flags } = uiConfig;
- const { location } = history;
- const { pathname } = location;
return (
Users
}
- >
+ />
{flags.RE && (
PROJECT ROLES
}
- >
+ />
)}
}
- >
+ />
}
- >
+ />
);
diff --git a/frontend/src/component/common/TabNav/TabNav.jsx b/frontend/src/component/common/TabNav/TabNav.jsx
index 47687c5618..43ee57f07d 100644
--- a/frontend/src/component/common/TabNav/TabNav.jsx
+++ b/frontend/src/component/common/TabNav/TabNav.jsx
@@ -61,6 +61,7 @@ const TabNav = ({ tabData, className, navClass, startingTab = 0 }) => {
TabNav.propTypes = {
tabData: PropTypes.array.isRequired,
+ navClass: PropTypes.string,
className: PropTypes.string,
startingTab: PropTypes.number,
};
diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap
index 9b57beca74..062bc1ea4c 100644
--- a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap
+++ b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap
@@ -398,12 +398,7 @@ Array [
"type": "protected",
},
Object {
- "component": Object {
- "$$typeof": Symbol(react.memo),
- "WrappedComponent": [Function],
- "compare": null,
- "type": [Function],
- },
+ "component": [Function],
"layout": "main",
"menu": Object {
"adminSettings": true,
diff --git a/frontend/src/component/menu/routes.js b/frontend/src/component/menu/routes.js
index 3560476aa8..26f375f20e 100644
--- a/frontend/src/component/menu/routes.js
+++ b/frontend/src/component/menu/routes.js
@@ -16,7 +16,7 @@ import Admin from '../admin';
import AdminApi from '../admin/api';
import AdminInvoice from '../admin/invoice/InvoiceAdminPage';
import AdminUsers from '../admin/users/UsersAdmin';
-import AdminAuth from '../admin/auth';
+import { AuthSettings } from '../admin/auth/AuthSettings';
import Login from '../user/Login/Login';
import { P, C, E, EEA, RE } from '../common/flags';
import NewUser from '../user/NewUser';
@@ -446,7 +446,7 @@ export const routes = [
path: '/admin/auth',
parent: '/admin',
title: 'Single Sign-On',
- component: AdminAuth,
+ component: AuthSettings,
type: 'protected',
layout: 'main',
menu: { adminSettings: true },
diff --git a/frontend/src/hooks/api/actions/useApi/useApi.ts b/frontend/src/hooks/api/actions/useApi/useApi.ts
index 6c68182107..8791587534 100644
--- a/frontend/src/hooks/api/actions/useApi/useApi.ts
+++ b/frontend/src/hooks/api/actions/useApi/useApi.ts
@@ -46,7 +46,7 @@ const useAPI = ({
handleUnauthorized,
propagateErrors = false,
}: IUseAPI) => {
- const [errors, setErrors] = useState({});
+ const [errors, setErrors] = useState>({});
const [loading, setLoading] = useState(false);
const defaultOptions: RequestInit = {
diff --git a/frontend/src/hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi.ts b/frontend/src/hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi.ts
index e679389436..9e2395d25b 100644
--- a/frontend/src/hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi.ts
+++ b/frontend/src/hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi.ts
@@ -12,7 +12,6 @@ export const handleBadRequest = async (
if (!setErrors) return;
if (res) {
const data = await res.json();
-
setErrors({message: data.message});
throw new Error(data.message);
}
diff --git a/frontend/src/hooks/api/getters/useUiConfig/defaultValue.ts b/frontend/src/hooks/api/getters/useUiConfig/defaultValue.ts
index 8d4adc70cc..f25ba0a13c 100644
--- a/frontend/src/hooks/api/getters/useUiConfig/defaultValue.ts
+++ b/frontend/src/hooks/api/getters/useUiConfig/defaultValue.ts
@@ -5,7 +5,7 @@ export const defaultValue = {
version: '3.x',
environment: '',
slogan: 'The enterprise ready feature toggle service.',
- flags: { P: false, C: false, E: false },
+ flags: { P: false, C: false, E: false, RE: false },
links: [
{
value: 'Documentation',
diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts
index 70ea96657a..20d16423dc 100644
--- a/frontend/src/interfaces/uiConfig.ts
+++ b/frontend/src/interfaces/uiConfig.ts
@@ -15,6 +15,7 @@ export interface IFlags {
C: boolean;
P: boolean;
E: boolean;
+ RE: boolean;
}
export interface IVersionInfo {
diff --git a/frontend/src/store/e-admin-auth/actions.js b/frontend/src/store/e-admin-auth/actions.js
deleted file mode 100644
index 5d91ceb428..0000000000
--- a/frontend/src/store/e-admin-auth/actions.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import api from './api';
-import { dispatchError } from '../util';
-export const RECIEVE_GOOGLE_CONFIG = 'RECIEVE_GOOGLE_CONFIG';
-export const RECIEVE_GOOGLE_CONFIG_ERROR = 'RECIEVE_GOOGLE_CONFIG_ERROR';
-export const UPDATE_GOOGLE_AUTH = 'UPDATE_GOOGLE_AUTH';
-export const UPDATE_GOOGLE_AUTH_ERROR = 'UPDATE_GOOGLE_AUTH_ERROR';
-export const RECIEVE_SAML_CONFIG = 'RECIEVE_SAML_CONFIG';
-export const RECIEVE_SAML_CONFIG_ERROR = 'RECIEVE_SAML_CONFIG_ERROR';
-export const UPDATE_SAML_AUTH = 'UPDATE_SAML_AUTH';
-export const UPDATE_SAML_AUTH_ERROR = 'UPDATE_SAML_AUTH_ERROR';
-export const RECIEVE_OIDC_CONFIG = 'RECIEVE_OIDC_CONFIG';
-export const RECIEVE_OIDC_CONFIG_ERROR = 'RECIEVE_OIDC_CONFIG_ERROR';
-export const UPDATE_OIDC_AUTH = 'UPDATE_OIDC_AUTH';
-export const UPDATE_OIDC_AUTH_ERROR = 'UPDATE_OIDC_AUTH_ERROR';
-
-const debug = require('debug')('unleash:e-admin-auth-actions');
-
-export function getGoogleConfig() {
- debug('Start fetching google-auth config');
- return dispatch =>
- api
- .getGoogleConfig()
- .then(config =>
- dispatch({
- type: RECIEVE_GOOGLE_CONFIG,
- config,
- })
- )
- .catch(dispatchError(dispatch, RECIEVE_GOOGLE_CONFIG_ERROR));
-}
-
-export function updateGoogleConfig(data) {
- return dispatch =>
- api
- .updateGoogleConfig(data)
- .then(config => dispatch({ type: UPDATE_GOOGLE_AUTH, config }))
- .catch(e => {
- dispatchError(dispatch, UPDATE_GOOGLE_AUTH_ERROR)(e);
- throw e;
- });
-}
-
-export function getSamlConfig() {
- debug('Start fetching Saml-auth config');
- return dispatch =>
- api
- .getSamlConfig()
- .then(config =>
- dispatch({
- type: RECIEVE_SAML_CONFIG,
- config,
- })
- )
- .catch(dispatchError(dispatch, RECIEVE_SAML_CONFIG_ERROR));
-}
-
-export function updateSamlConfig(data) {
- return dispatch =>
- api
- .updateSamlConfig(data)
- .then(config => dispatch({ type: UPDATE_SAML_AUTH, config }))
- .catch(e => {
- dispatchError(dispatch, UPDATE_SAML_AUTH_ERROR)(e);
- throw e;
- });
-}
-
-export function getOidcConfig() {
- debug('Start fetching OIDC-auth config');
- return dispatch =>
- api
- .getOidcConfig()
- .then(config =>
- dispatch({
- type: RECIEVE_OIDC_CONFIG,
- config,
- })
- )
- .catch(dispatchError(dispatch, RECIEVE_OIDC_CONFIG_ERROR));
-}
-
-export function updateOidcConfig(data) {
- return dispatch =>
- api
- .updateOidcConfig(data)
- .then(config => dispatch({ type: UPDATE_OIDC_AUTH, config }))
- .catch(e => {
- dispatchError(dispatch, UPDATE_OIDC_AUTH_ERROR)(e);
- throw e;
- });
-}
\ No newline at end of file
diff --git a/frontend/src/store/e-admin-auth/api.js b/frontend/src/store/e-admin-auth/api.js
deleted file mode 100644
index 35333d6c06..0000000000
--- a/frontend/src/store/e-admin-auth/api.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import { throwIfNotSuccess, headers } from '../api-helper';
-import { formatApiPath } from '../../utils/format-path';
-
-const GOOGLE_URI = formatApiPath('api/admin/auth/google/settings');
-const SAML_URI = formatApiPath('api/admin/auth/saml/settings');
-const OIDC_URI = formatApiPath('api/admin/auth/oidc/settings');
-
-function getGoogleConfig() {
- return fetch(GOOGLE_URI, { headers, credentials: 'include' })
- .then(throwIfNotSuccess)
- .then(response => response.json());
-}
-
-function updateGoogleConfig(data) {
- return fetch(GOOGLE_URI, {
- method: 'POST',
- headers,
- body: JSON.stringify(data),
- credentials: 'include',
- })
- .then(throwIfNotSuccess)
- .then(response => response.json());
-}
-
-function getSamlConfig() {
- return fetch(SAML_URI, { headers, credentials: 'include' })
- .then(throwIfNotSuccess)
- .then(response => response.json());
-}
-
-function updateSamlConfig(data) {
- return fetch(SAML_URI, {
- method: 'POST',
- headers,
- body: JSON.stringify(data),
- credentials: 'include',
- })
- .then(throwIfNotSuccess)
- .then(response => response.json());
-}
-
-function getOidcConfig() {
- return fetch(OIDC_URI, { headers, credentials: 'include' })
- .then(throwIfNotSuccess)
- .then(response => response.json());
-}
-
-function updateOidcConfig(data) {
- return fetch(OIDC_URI, {
- method: 'POST',
- headers,
- body: JSON.stringify(data),
- credentials: 'include',
- })
- .then(throwIfNotSuccess)
- .then(response => response.json());
-}
-
-export default {
- getGoogleConfig,
- updateGoogleConfig,
- getSamlConfig,
- updateSamlConfig,
- getOidcConfig,
- updateOidcConfig,
-};
diff --git a/frontend/src/store/e-admin-auth/index.js b/frontend/src/store/e-admin-auth/index.js
deleted file mode 100644
index 4f0a546000..0000000000
--- a/frontend/src/store/e-admin-auth/index.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Map as $Map } from 'immutable';
-import { RECIEVE_GOOGLE_CONFIG, UPDATE_GOOGLE_AUTH, RECIEVE_SAML_CONFIG, UPDATE_SAML_AUTH, UPDATE_OIDC_AUTH, RECIEVE_OIDC_CONFIG } from './actions';
-
-const store = (state = new $Map({ google: {}, saml: {}, oidc: {} }), action) => {
- switch (action.type) {
- case UPDATE_GOOGLE_AUTH:
- case RECIEVE_GOOGLE_CONFIG:
- return state.set('google', action.config);
- case UPDATE_SAML_AUTH:
- case RECIEVE_SAML_CONFIG:
- return state.set('saml', action.config);
- case UPDATE_OIDC_AUTH:
- case RECIEVE_OIDC_CONFIG:
- return state.set('oidc', action.config);
- default:
- return state;
- }
-};
-
-export default store;
diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js
index b329b9bab4..2949742dbe 100644
--- a/frontend/src/store/index.js
+++ b/frontend/src/store/index.js
@@ -15,7 +15,6 @@ import uiConfig from './ui-config';
import context from './context';
import projects from './project';
import addons from './addons';
-import authAdmin from './e-admin-auth';
import apiCalls from './api-calls';
import invoiceAdmin from './e-admin-invoice';
import feedback from './feedback';
@@ -37,7 +36,6 @@ const unleashStore = combineReducers({
context,
projects,
addons,
- authAdmin,
apiCalls,
invoiceAdmin,
feedback,
diff --git a/frontend/src/utils/format-unknown-error.test.ts b/frontend/src/utils/format-unknown-error.test.ts
new file mode 100644
index 0000000000..1e12a3153a
--- /dev/null
+++ b/frontend/src/utils/format-unknown-error.test.ts
@@ -0,0 +1,8 @@
+import { formatUnknownError } from './format-unknown-error';
+
+test('formatUnknownError', () => {
+ expect(formatUnknownError(1)).toEqual('Unknown error');
+ expect(formatUnknownError('1')).toEqual('1');
+ expect(formatUnknownError(new Error('1'))).toEqual('1');
+ expect(formatUnknownError(new Error())).toEqual('Error');
+});
diff --git a/frontend/src/utils/format-unknown-error.ts b/frontend/src/utils/format-unknown-error.ts
new file mode 100644
index 0000000000..d4a49e8586
--- /dev/null
+++ b/frontend/src/utils/format-unknown-error.ts
@@ -0,0 +1,10 @@
+// Get a human-readable error message string from a caught value.
+export const formatUnknownError = (error: unknown): string => {
+ if (error instanceof Error) {
+ return error.message || error.toString();
+ } else if (typeof error === 'string') {
+ return error;
+ } else {
+ return 'Unknown error';
+ }
+};
diff --git a/frontend/src/utils/remove-empty-string-fields.test.ts b/frontend/src/utils/remove-empty-string-fields.test.ts
new file mode 100644
index 0000000000..8a9fd199cf
--- /dev/null
+++ b/frontend/src/utils/remove-empty-string-fields.test.ts
@@ -0,0 +1,11 @@
+import { removeEmptyStringFields } from './remove-empty-string-fields';
+
+test('removeEmptyStringFields', () => {
+ expect(removeEmptyStringFields({})).toEqual({});
+ expect(removeEmptyStringFields({ a: undefined })).toEqual({ a: undefined });
+ expect(removeEmptyStringFields({ a: 0 })).toEqual({ a: 0 });
+ expect(removeEmptyStringFields({ a: 1 })).toEqual({ a: 1 });
+ expect(removeEmptyStringFields({ a: '1' })).toEqual({ a: '1' });
+ expect(removeEmptyStringFields({ a: '' })).toEqual({});
+ expect(removeEmptyStringFields({ a: '', b: '2' })).toEqual({ b: '2' });
+});
diff --git a/frontend/src/utils/remove-empty-string-fields.ts b/frontend/src/utils/remove-empty-string-fields.ts
new file mode 100644
index 0000000000..c989cc2deb
--- /dev/null
+++ b/frontend/src/utils/remove-empty-string-fields.ts
@@ -0,0 +1,9 @@
+// Remove fields from an object if their value is the empty string.
+export const removeEmptyStringFields = (object: {
+ [key: string]: unknown;
+}): { [key: string]: unknown } => {
+ const entries = Object.entries(object);
+ const filtered = entries.filter(([, v]) => v !== '');
+
+ return Object.fromEntries(filtered);
+};