mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Merge branch 'main' into refactor/applications
This commit is contained in:
		
						commit
						f58c284f70
					
				@ -76,6 +76,7 @@
 | 
			
		||||
    "react-dnd": "14.0.5",
 | 
			
		||||
    "react-dnd-html5-backend": "14.1.0",
 | 
			
		||||
    "react-dom": "17.0.2",
 | 
			
		||||
    "react-hooks-global-state": "^1.0.2",
 | 
			
		||||
    "react-outside-click-handler": "1.3.0",
 | 
			
		||||
    "react-redux": "7.2.6",
 | 
			
		||||
    "react-router-dom": "5.3.0",
 | 
			
		||||
 | 
			
		||||
@ -31,6 +31,7 @@ import Dialogue from '../../../common/Dialogue';
 | 
			
		||||
import { CREATE_API_TOKEN_BUTTON } from '../../../../testIds';
 | 
			
		||||
import { Alert } from '@material-ui/lab';
 | 
			
		||||
import copy from 'copy-to-clipboard';
 | 
			
		||||
import { useLocationSettings } from "../../../../hooks/useLocationSettings";
 | 
			
		||||
 | 
			
		||||
interface IApiToken {
 | 
			
		||||
    createdAt: Date;
 | 
			
		||||
@ -41,16 +42,13 @@ interface IApiToken {
 | 
			
		||||
    environment: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IApiTokenList {
 | 
			
		||||
    location: any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ApiTokenList = ({ location }: IApiTokenList) => {
 | 
			
		||||
const ApiTokenList = () => {
 | 
			
		||||
    const styles = useStyles();
 | 
			
		||||
    const { hasAccess } = useContext(AccessContext);
 | 
			
		||||
    const { uiConfig } = useUiConfig();
 | 
			
		||||
    const [showDelete, setShowDelete] = useState(false);
 | 
			
		||||
    const [delToken, setDeleteToken] = useState<IApiToken>();
 | 
			
		||||
    const { locationSettings } = useLocationSettings()
 | 
			
		||||
    const { setToastData } = useToast();
 | 
			
		||||
    const { tokens, loading, refetch, error } = useApiTokens();
 | 
			
		||||
    const { deleteToken } = useApiTokensApi();
 | 
			
		||||
@ -150,7 +148,7 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
 | 
			
		||||
                                >
 | 
			
		||||
                                    {formatDateWithLocale(
 | 
			
		||||
                                        item.createdAt,
 | 
			
		||||
                                        location.locale
 | 
			
		||||
                                        locationSettings.locale
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </TableCell>
 | 
			
		||||
                                <TableCell
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import AdminMenu from '../menu/AdminMenu';
 | 
			
		||||
import usePermissions from '../../../hooks/usePermissions';
 | 
			
		||||
import ConditionallyRender from '../../common/ConditionallyRender';
 | 
			
		||||
 | 
			
		||||
const ApiPage = ({ history, location }) => {
 | 
			
		||||
const ApiPage = ({ history }) => {
 | 
			
		||||
    const { isAdmin } = usePermissions();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
@ -14,7 +14,7 @@ const ApiPage = ({ history, location }) => {
 | 
			
		||||
                condition={isAdmin()}
 | 
			
		||||
                show={<AdminMenu history={history} />}
 | 
			
		||||
            />
 | 
			
		||||
            <ApiTokenList location={location} />
 | 
			
		||||
            <ApiTokenList />
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -22,7 +22,6 @@ const ApiPage = ({ history, location }) => {
 | 
			
		||||
ApiPage.propTypes = {
 | 
			
		||||
    match: PropTypes.object.isRequired,
 | 
			
		||||
    history: PropTypes.object.isRequired,
 | 
			
		||||
    location: PropTypes.object.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ApiPage;
 | 
			
		||||
 | 
			
		||||
@ -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: <PasswordAuthSettings />,
 | 
			
		||||
            component: <PasswordAuth />,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            label: 'Google',
 | 
			
		||||
@ -32,7 +34,7 @@ function AdminAuthPage({ authenticationType, history }) {
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div>
 | 
			
		||||
            <AdminMenu history={history} />
 | 
			
		||||
            <AdminMenu />
 | 
			
		||||
            <PageContent headerContent="Single Sign-On">
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={authenticationType === 'enterprise'}
 | 
			
		||||
@ -80,12 +82,4 @@ function AdminAuthPage({ authenticationType, history }) {
 | 
			
		||||
            </PageContent>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AdminAuthPage.propTypes = {
 | 
			
		||||
    match: PropTypes.object.isRequired,
 | 
			
		||||
    history: PropTypes.object.isRequired,
 | 
			
		||||
    authenticationType: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default AdminAuthPage;
 | 
			
		||||
@ -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<HTMLInputElement>) => {
 | 
			
		||||
        setValue(e.target.name, e.target.value);
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
return (
 | 
			
		||||
    <Fragment>
 | 
			
		||||
        <Grid container spacing={3}>
 | 
			
		||||
            <Grid item md={5}>
 | 
			
		||||
                <strong>Auto-create users</strong>
 | 
			
		||||
                <p>
 | 
			
		||||
                    Enable automatic creation of new users when signing in.
 | 
			
		||||
                </p>
 | 
			
		||||
    return (
 | 
			
		||||
        <Fragment>
 | 
			
		||||
            <Grid container spacing={3}>
 | 
			
		||||
                <Grid item md={5}>
 | 
			
		||||
                    <strong>Auto-create users</strong>
 | 
			
		||||
                    <p>
 | 
			
		||||
                        Enable automatic creation of new users when signing in.
 | 
			
		||||
                    </p>
 | 
			
		||||
                </Grid>
 | 
			
		||||
                <Grid item md={6} style={{ padding: '20px' }}>
 | 
			
		||||
                    <FormControlLabel
 | 
			
		||||
                        control={
 | 
			
		||||
                            <Switch
 | 
			
		||||
                                onChange={updateAutoCreate}
 | 
			
		||||
                                name="enabled"
 | 
			
		||||
                                checked={data.autoCreate}
 | 
			
		||||
                                disabled={!data.enabled}
 | 
			
		||||
                            />
 | 
			
		||||
                        }
 | 
			
		||||
                        label="Auto-create users"
 | 
			
		||||
                    />
 | 
			
		||||
                </Grid>
 | 
			
		||||
            </Grid>
 | 
			
		||||
            <Grid item md={6} style={{ padding: '20px' }}>
 | 
			
		||||
 | 
			
		||||
            <FormControlLabel
 | 
			
		||||
                control={ <Switch
 | 
			
		||||
                    onChange={updateAutoCreate}
 | 
			
		||||
                    name="enabled"
 | 
			
		||||
                    checked={data.autoCreate}
 | 
			
		||||
                    disabled={!data.enabled}
 | 
			
		||||
                />}
 | 
			
		||||
                label="Auto-create users"
 | 
			
		||||
            />
 | 
			
		||||
            <Grid container spacing={3}>
 | 
			
		||||
                <Grid item md={5}>
 | 
			
		||||
                    <strong>Default Root Role</strong>
 | 
			
		||||
                    <p>
 | 
			
		||||
                        Choose which root role the user should get when no
 | 
			
		||||
                        explicit role mapping exists.
 | 
			
		||||
                    </p>
 | 
			
		||||
                </Grid>
 | 
			
		||||
                <Grid item md={6}>
 | 
			
		||||
                    <FormControl style={{ minWidth: '200px' }}>
 | 
			
		||||
                        <InputLabel id="defaultRootRole-label">
 | 
			
		||||
                            Default Role
 | 
			
		||||
                        </InputLabel>
 | 
			
		||||
                        <Select
 | 
			
		||||
                            labelId="defaultRootRole-label"
 | 
			
		||||
                            id="defaultRootRole"
 | 
			
		||||
                            name="defaultRootRole"
 | 
			
		||||
                            disabled={!data.autoCreate || !data.enabled}
 | 
			
		||||
                            value={data.defaultRootRole || 'Editor'}
 | 
			
		||||
                            onChange={updateDefaultRootRole}
 | 
			
		||||
                        >
 | 
			
		||||
                            {/*consider these from API or constants. */}
 | 
			
		||||
                            <MenuItem value="Viewer">Viewer</MenuItem>
 | 
			
		||||
                            <MenuItem value="Editor">Editor</MenuItem>
 | 
			
		||||
                            <MenuItem value="Admin">Admin</MenuItem>
 | 
			
		||||
                        </Select>
 | 
			
		||||
                    </FormControl>
 | 
			
		||||
                </Grid>
 | 
			
		||||
            </Grid>
 | 
			
		||||
        </Grid>
 | 
			
		||||
        <Grid container spacing={3}>
 | 
			
		||||
            <Grid item md={5}>
 | 
			
		||||
                <strong>Default Root Role</strong>
 | 
			
		||||
                <p>
 | 
			
		||||
                    Choose which root role the user should get when no explicit role mapping exists.
 | 
			
		||||
                </p>
 | 
			
		||||
            <Grid container spacing={3}>
 | 
			
		||||
                <Grid item md={5}>
 | 
			
		||||
                    <strong>Email domains</strong>
 | 
			
		||||
                    <p>
 | 
			
		||||
                        Comma separated list of email domains that should be
 | 
			
		||||
                        allowed to sign in.
 | 
			
		||||
                    </p>
 | 
			
		||||
                </Grid>
 | 
			
		||||
                <Grid item md={6}>
 | 
			
		||||
                    <TextField
 | 
			
		||||
                        onChange={updateField}
 | 
			
		||||
                        label="Email domains"
 | 
			
		||||
                        name="emailDomains"
 | 
			
		||||
                        disabled={!data.autoCreate || !data.enabled}
 | 
			
		||||
                        required={!!data.autoCreate}
 | 
			
		||||
                        value={data.emailDomains || ''}
 | 
			
		||||
                        placeholder="@company.com, @anotherCompany.com"
 | 
			
		||||
                        style={{ width: '400px' }}
 | 
			
		||||
                        rows={2}
 | 
			
		||||
                        variant="outlined"
 | 
			
		||||
                        size="small"
 | 
			
		||||
                    />
 | 
			
		||||
                </Grid>
 | 
			
		||||
            </Grid>
 | 
			
		||||
            <Grid item md={6}>
 | 
			
		||||
            <FormControl style={{minWidth: '200px'}}>
 | 
			
		||||
                <InputLabel id="defaultRootRole-label">Default Role</InputLabel>
 | 
			
		||||
                <Select
 | 
			
		||||
                    labelId="defaultRootRole-label"
 | 
			
		||||
                    id="defaultRootRole"
 | 
			
		||||
                    name="defaultRootRole"
 | 
			
		||||
                    disabled={!data.autoCreate || !data.enabled}
 | 
			
		||||
                    value={data.defaultRootRole || 'Editor'}
 | 
			
		||||
                    onChange={updateDefaultRootRole}
 | 
			
		||||
                    >
 | 
			
		||||
                    {/*consider these from API or constants. */}
 | 
			
		||||
                    <MenuItem value='Viewer'>Viewer</MenuItem>
 | 
			
		||||
                    <MenuItem value='Editor'>Editor</MenuItem>
 | 
			
		||||
                    <MenuItem value='Admin'>Admin</MenuItem>
 | 
			
		||||
                </Select>
 | 
			
		||||
            </FormControl>
 | 
			
		||||
            </Grid>
 | 
			
		||||
        </Grid>
 | 
			
		||||
        <Grid container spacing={3}>
 | 
			
		||||
            <Grid item md={5}>
 | 
			
		||||
                <strong>Email domains</strong>
 | 
			
		||||
                <p>
 | 
			
		||||
                    Comma separated list of email domains
 | 
			
		||||
                    that should be allowed to sign in.
 | 
			
		||||
                </p>
 | 
			
		||||
            </Grid>
 | 
			
		||||
            <Grid item md={6}>
 | 
			
		||||
                <TextField
 | 
			
		||||
                    onChange={updateField}
 | 
			
		||||
                    label="Email domains"
 | 
			
		||||
                    name="emailDomains"
 | 
			
		||||
                    disabled={!data.autoCreate || !data.enabled}
 | 
			
		||||
                    required={!!data.autoCreate}
 | 
			
		||||
                    value={data.emailDomains || ''}
 | 
			
		||||
                    placeholder="@company.com, @anotherCompany.com"
 | 
			
		||||
                    style={{ width: '400px' }}
 | 
			
		||||
                    rows={2}
 | 
			
		||||
                    variant="outlined"
 | 
			
		||||
                    size="small"
 | 
			
		||||
                />
 | 
			
		||||
            </Grid>
 | 
			
		||||
        </Grid>
 | 
			
		||||
    </Fragment>);
 | 
			
		||||
}
 | 
			
		||||
export default AutoCreateForm;
 | 
			
		||||
        </Fragment>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -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 <span>You need admin privileges to access this section.</span>;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const updateField = e => {
 | 
			
		||||
    const updateField = (event: React.ChangeEvent<HTMLInputElement>) => {
 | 
			
		||||
        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 (
 | 
			
		||||
        <PageContent>
 | 
			
		||||
        <PageContent headerContent="">
 | 
			
		||||
            <Grid container style={{ marginBottom: '1rem' }}>
 | 
			
		||||
                <Grid item xs={12}>
 | 
			
		||||
                    <Alert severity="info">
 | 
			
		||||
@ -84,7 +88,7 @@ function GoogleAuth({
 | 
			
		||||
                        </a>{' '}
 | 
			
		||||
                        to learn how to integrate with Google OAuth 2.0. <br />
 | 
			
		||||
                        Callback URL:{' '}
 | 
			
		||||
                        <code>{unleashUrl}/auth/google/callback</code>
 | 
			
		||||
                        <code>{uiConfig.unleashUrl}/auth/google/callback</code>
 | 
			
		||||
                    </Alert>
 | 
			
		||||
                </Grid>
 | 
			
		||||
            </Grid>
 | 
			
		||||
@ -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
 | 
			
		||||
                        </Switch>
 | 
			
		||||
                        />
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                </Grid>
 | 
			
		||||
                <Grid container spacing={3}>
 | 
			
		||||
@ -229,22 +231,18 @@ function GoogleAuth({
 | 
			
		||||
                            variant="contained"
 | 
			
		||||
                            color="primary"
 | 
			
		||||
                            type="submit"
 | 
			
		||||
                            disabled={loading}
 | 
			
		||||
                        >
 | 
			
		||||
                            Save
 | 
			
		||||
                        </Button>{' '}
 | 
			
		||||
                        <small>{info}</small>
 | 
			
		||||
                        <p>
 | 
			
		||||
                            <small style={{ color: 'red' }}>
 | 
			
		||||
                                {errors?.message}
 | 
			
		||||
                            </small>
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                </Grid>
 | 
			
		||||
            </form>
 | 
			
		||||
        </PageContent>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
GoogleAuth.propTypes = {
 | 
			
		||||
    config: PropTypes.object,
 | 
			
		||||
    unleashUrl: PropTypes.string,
 | 
			
		||||
    getGoogleConfig: PropTypes.func.isRequired,
 | 
			
		||||
    updateGoogleConfig: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default GoogleAuth;
 | 
			
		||||
@ -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<HTMLInputElement>) => {
 | 
			
		||||
        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 (
 | 
			
		||||
        <PageContent>
 | 
			
		||||
        <PageContent headerContent="">
 | 
			
		||||
            <Grid container style={{ marginBottom: '1rem' }}>
 | 
			
		||||
                <Grid item md={12}>
 | 
			
		||||
                    <Alert severity="info">
 | 
			
		||||
@ -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). <br />
 | 
			
		||||
                        Callback URL:{' '}
 | 
			
		||||
                        <code>{unleashUrl}/auth/oidc/callback</code>
 | 
			
		||||
                        <code>{uiConfig.unleashUrl}/auth/oidc/callback</code>
 | 
			
		||||
                    </Alert>
 | 
			
		||||
                </Grid>
 | 
			
		||||
            </Grid>
 | 
			
		||||
@ -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 }) {
 | 
			
		||||
                <Grid container spacing={3}>
 | 
			
		||||
                    <Grid item md={5}>
 | 
			
		||||
                        <strong>Enable Single Sign-Out</strong>
 | 
			
		||||
                        <p>If you enable Single Sign-Out Unleash will redirect the user to the IDP as part of the Sign-out process.</p>
 | 
			
		||||
                        <p>
 | 
			
		||||
                            If you enable Single Sign-Out Unleash will redirect
 | 
			
		||||
                            the user to the IDP as part of the Sign-out process.
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                    <Grid item md={6} style={{ padding: '20px' }}>
 | 
			
		||||
                        <FormControlLabel
 | 
			
		||||
@ -204,15 +213,21 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
 | 
			
		||||
                <Grid container spacing={3}>
 | 
			
		||||
                    <Grid item md={5}>
 | 
			
		||||
                        <strong>ACR Values</strong>
 | 
			
		||||
                        <p>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.</p>
 | 
			
		||||
                        <p>
 | 
			
		||||
                            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.
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                    <Grid item md={6}>
 | 
			
		||||
                        <TextField
 | 
			
		||||
                            onChange={updateField}
 | 
			
		||||
                            label="ACR Values"
 | 
			
		||||
                            name="acrValues"
 | 
			
		||||
                            value={data.acrValues || ''}
 | 
			
		||||
                            value={data.acrValues}
 | 
			
		||||
                            disabled={!data.enabled}
 | 
			
		||||
                            style={{ width: '400px' }}
 | 
			
		||||
                            variant="outlined"
 | 
			
		||||
@ -229,23 +244,18 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
 | 
			
		||||
                            variant="contained"
 | 
			
		||||
                            color="primary"
 | 
			
		||||
                            type="submit"
 | 
			
		||||
                            disabled={loading}
 | 
			
		||||
                        >
 | 
			
		||||
                            Save
 | 
			
		||||
                        </Button>{' '}
 | 
			
		||||
                        <small>{info}</small>
 | 
			
		||||
                        <small style={{ color: 'red' }}>{error}</small>
 | 
			
		||||
                        <p>
 | 
			
		||||
                            <small style={{ color: 'red' }}>
 | 
			
		||||
                                {errors?.message}
 | 
			
		||||
                            </small>
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                </Grid>
 | 
			
		||||
            </form>
 | 
			
		||||
        </PageContent>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
OidcAuth.propTypes = {
 | 
			
		||||
    config: PropTypes.object,
 | 
			
		||||
    unleash: PropTypes.string,
 | 
			
		||||
    getOidcConfig: PropTypes.func.isRequired,
 | 
			
		||||
    updateOidcConfig: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default OidcAuth;
 | 
			
		||||
@ -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<boolean>(false);
 | 
			
		||||
    const { updateSettings, errors, loading } = useAuthSettingsApi<ISimpleAuthSettings>('simple')
 | 
			
		||||
    const [disablePasswordAuth, setDisablePasswordAuth] =
 | 
			
		||||
        useState<boolean>(false);
 | 
			
		||||
    const { updateSettings, errors, loading } =
 | 
			
		||||
        useAuthSettingsApi<ISimpleAuthSettings>('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 (
 | 
			
		||||
        <PageContent headerContent=''>
 | 
			
		||||
        <PageContent headerContent="">
 | 
			
		||||
            <form onSubmit={onSubmit}>
 | 
			
		||||
                <Grid container spacing={3}>
 | 
			
		||||
                    <Grid item md={5}>
 | 
			
		||||
@ -82,7 +74,9 @@ const PasswordAuthSettings = () => {
 | 
			
		||||
                                    checked={!disablePasswordAuth}
 | 
			
		||||
                                />
 | 
			
		||||
                            }
 | 
			
		||||
                            label={!disablePasswordAuth ? 'Enabled' : 'Disabled'}
 | 
			
		||||
                            label={
 | 
			
		||||
                                !disablePasswordAuth ? 'Enabled' : 'Disabled'
 | 
			
		||||
                            }
 | 
			
		||||
                        />
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                </Grid>
 | 
			
		||||
@ -96,12 +90,14 @@ const PasswordAuthSettings = () => {
 | 
			
		||||
                        >
 | 
			
		||||
                            Save
 | 
			
		||||
                        </Button>{' '}
 | 
			
		||||
                        <p><small style={{ color: 'red' }}>{errors?.message}</small></p>
 | 
			
		||||
                        <p>
 | 
			
		||||
                            <small style={{ color: 'red' }}>
 | 
			
		||||
                                {errors?.message}
 | 
			
		||||
                            </small>
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                </Grid>
 | 
			
		||||
            </form>
 | 
			
		||||
        </PageContent>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default PasswordAuthSettings;
 | 
			
		||||
};
 | 
			
		||||
@ -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<HTMLInputElement>) => {
 | 
			
		||||
        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 (
 | 
			
		||||
        <PageContent>
 | 
			
		||||
        <PageContent headerContent="">
 | 
			
		||||
            <Grid container style={{ marginBottom: '1rem' }}>
 | 
			
		||||
                <Grid item md={12}>
 | 
			
		||||
                    <Alert severity="info">
 | 
			
		||||
@ -86,7 +96,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
 | 
			
		||||
                        to learn how to integrate with specific SAML 2.0
 | 
			
		||||
                        providers (Okta, Keycloak, etc). <br />
 | 
			
		||||
                        Callback URL:{' '}
 | 
			
		||||
                        <code>{unleashUrl}/auth/saml/callback</code>
 | 
			
		||||
                        <code>{uiConfig.unleashUrl}/auth/saml/callback</code>
 | 
			
		||||
                    </Alert>
 | 
			
		||||
                </Grid>
 | 
			
		||||
            </Grid>
 | 
			
		||||
@ -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
 | 
			
		||||
                        </Button>{' '}
 | 
			
		||||
                        <small>{info}</small>
 | 
			
		||||
                        <p>
 | 
			
		||||
                            <small style={{ color: 'red' }}>
 | 
			
		||||
                                {errors?.message}
 | 
			
		||||
                            </small>
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                </Grid>
 | 
			
		||||
            </form>
 | 
			
		||||
        </PageContent>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SamlAuth.propTypes = {
 | 
			
		||||
    config: PropTypes.object,
 | 
			
		||||
    unleash: PropTypes.string,
 | 
			
		||||
    getSamlConfig: PropTypes.func.isRequired,
 | 
			
		||||
    updateSamlConfig: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default SamlAuth;
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -14,15 +14,15 @@ import HeaderTitle from '../../common/HeaderTitle';
 | 
			
		||||
import ConditionallyRender from '../../common/ConditionallyRender';
 | 
			
		||||
import { formatApiPath } from '../../../utils/format-path';
 | 
			
		||||
import useInvoices from '../../../hooks/api/getters/useInvoices/useInvoices';
 | 
			
		||||
import { useLocation } from 'react-router-dom';
 | 
			
		||||
import { IInvoice } from '../../../interfaces/invoice';
 | 
			
		||||
import { useLocationSettings } from '../../../hooks/useLocationSettings';
 | 
			
		||||
 | 
			
		||||
const PORTAL_URL = formatApiPath('api/admin/invoices/portal');
 | 
			
		||||
 | 
			
		||||
const InvoiceList = () => {
 | 
			
		||||
    const { refetchInvoices, invoices } = useInvoices();
 | 
			
		||||
    const [isLoaded, setLoaded] = useState(false);
 | 
			
		||||
    const location = useLocation();
 | 
			
		||||
    const { locationSettings } = useLocationSettings();
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        refetchInvoices();
 | 
			
		||||
@ -89,7 +89,7 @@ const InvoiceList = () => {
 | 
			
		||||
                                            {item.dueDate &&
 | 
			
		||||
                                                formatDateWithLocale(
 | 
			
		||||
                                                    item.dueDate,
 | 
			
		||||
                                                    location.locale
 | 
			
		||||
                                                    locationSettings.locale
 | 
			
		||||
                                                )}
 | 
			
		||||
                                        </TableCell>
 | 
			
		||||
                                        <TableCell
 | 
			
		||||
 | 
			
		||||
@ -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 (
 | 
			
		||||
        <Paper
 | 
			
		||||
            style={{
 | 
			
		||||
@ -45,7 +44,7 @@ function AdminMenu({ history }) {
 | 
			
		||||
                            <span>Users</span>
 | 
			
		||||
                        </NavLink>
 | 
			
		||||
                    }
 | 
			
		||||
                ></Tab>
 | 
			
		||||
                />
 | 
			
		||||
                {flags.RE && (
 | 
			
		||||
                    <Tab
 | 
			
		||||
                        value="/admin/roles"
 | 
			
		||||
@ -58,7 +57,7 @@ function AdminMenu({ history }) {
 | 
			
		||||
                                <span>PROJECT ROLES</span>
 | 
			
		||||
                            </NavLink>
 | 
			
		||||
                        }
 | 
			
		||||
                    ></Tab>
 | 
			
		||||
                    />
 | 
			
		||||
                )}
 | 
			
		||||
 | 
			
		||||
                <Tab
 | 
			
		||||
@ -72,7 +71,7 @@ function AdminMenu({ history }) {
 | 
			
		||||
                            API Access
 | 
			
		||||
                        </NavLink>
 | 
			
		||||
                    }
 | 
			
		||||
                ></Tab>
 | 
			
		||||
                />
 | 
			
		||||
                <Tab
 | 
			
		||||
                    value="/admin/auth"
 | 
			
		||||
                    label={
 | 
			
		||||
@ -84,7 +83,7 @@ function AdminMenu({ history }) {
 | 
			
		||||
                            Single Sign-On
 | 
			
		||||
                        </NavLink>
 | 
			
		||||
                    }
 | 
			
		||||
                ></Tab>
 | 
			
		||||
                />
 | 
			
		||||
            </Tabs>
 | 
			
		||||
        </Paper>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@ import AccessContext from '../../../../../contexts/AccessContext';
 | 
			
		||||
import { IUser } from '../../../../../interfaces/user';
 | 
			
		||||
import { useStyles } from './UserListItem.styles';
 | 
			
		||||
import { useHistory } from 'react-router-dom';
 | 
			
		||||
import { ILocationSettings } from "../../../../../hooks/useLocationSettings";
 | 
			
		||||
 | 
			
		||||
interface IUserListItemProps {
 | 
			
		||||
    user: IUser;
 | 
			
		||||
@ -21,11 +22,7 @@ interface IUserListItemProps {
 | 
			
		||||
    openUpdateDialog: (user: IUser) => (e: SyntheticEvent) => void;
 | 
			
		||||
    openPwDialog: (user: IUser) => (e: SyntheticEvent) => void;
 | 
			
		||||
    openDelDialog: (user: IUser) => (e: SyntheticEvent) => void;
 | 
			
		||||
    location: ILocation;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ILocation {
 | 
			
		||||
    locale: string;
 | 
			
		||||
    locationSettings: ILocationSettings;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const UserListItem = ({
 | 
			
		||||
@ -34,7 +31,7 @@ const UserListItem = ({
 | 
			
		||||
    openDelDialog,
 | 
			
		||||
    openPwDialog,
 | 
			
		||||
    openUpdateDialog,
 | 
			
		||||
    location,
 | 
			
		||||
    locationSettings,
 | 
			
		||||
}: IUserListItemProps) => {
 | 
			
		||||
    const { hasAccess } = useContext(AccessContext);
 | 
			
		||||
    const history = useHistory()
 | 
			
		||||
@ -54,7 +51,7 @@ const UserListItem = ({
 | 
			
		||||
            </TableCell>
 | 
			
		||||
            <TableCell>
 | 
			
		||||
                <span data-loading>
 | 
			
		||||
                    {formatDateWithLocale(user.createdAt, location.locale)}
 | 
			
		||||
                    {formatDateWithLocale(user.createdAt, locationSettings.locale)}
 | 
			
		||||
                </span>
 | 
			
		||||
            </TableCell>
 | 
			
		||||
            <TableCell className={styles.leftTableCell}>
 | 
			
		||||
 | 
			
		||||
@ -20,10 +20,10 @@ import loadingData from './loadingData';
 | 
			
		||||
import useLoading from '../../../../hooks/useLoading';
 | 
			
		||||
import usePagination from '../../../../hooks/usePagination';
 | 
			
		||||
import PaginateUI from '../../../common/PaginateUI/PaginateUI';
 | 
			
		||||
import { useHistory } from 'react-router-dom';
 | 
			
		||||
import { IUser } from '../../../../interfaces/user';
 | 
			
		||||
import IRole from '../../../../interfaces/role';
 | 
			
		||||
import useToast from '../../../../hooks/useToast';
 | 
			
		||||
import { useLocationSettings } from "../../../../hooks/useLocationSettings";
 | 
			
		||||
 | 
			
		||||
const UsersList = () => {
 | 
			
		||||
    const { users, roles, refetch, loading } = useUsers();
 | 
			
		||||
@ -35,9 +35,8 @@ const UsersList = () => {
 | 
			
		||||
        userLoading,
 | 
			
		||||
        userApiErrors,
 | 
			
		||||
    } = useAdminUsersApi();
 | 
			
		||||
    const history = useHistory();
 | 
			
		||||
    const { location } = history;
 | 
			
		||||
    const { hasAccess } = useContext(AccessContext);
 | 
			
		||||
    const { locationSettings } = useLocationSettings()
 | 
			
		||||
    const [pwDialog, setPwDialog] = useState<{ open: boolean; user?: IUser }>({
 | 
			
		||||
        open: false,
 | 
			
		||||
    });
 | 
			
		||||
@ -104,7 +103,7 @@ const UsersList = () => {
 | 
			
		||||
                    user={user}
 | 
			
		||||
                    openPwDialog={openPwDialog}
 | 
			
		||||
                    openDelDialog={openDelDialog}
 | 
			
		||||
                    location={location}
 | 
			
		||||
                    locationSettings={locationSettings}
 | 
			
		||||
                    renderRole={renderRole}
 | 
			
		||||
                />
 | 
			
		||||
            ));
 | 
			
		||||
@ -117,7 +116,7 @@ const UsersList = () => {
 | 
			
		||||
                    user={user}
 | 
			
		||||
                    openPwDialog={openPwDialog}
 | 
			
		||||
                    openDelDialog={openDelDialog}
 | 
			
		||||
                    location={location}
 | 
			
		||||
                    locationSettings={locationSettings}
 | 
			
		||||
                    renderRole={renderRole}
 | 
			
		||||
                />
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ test('renders correctly if no application', () => {
 | 
			
		||||
                    storeApplicationMetaData={jest.fn()}
 | 
			
		||||
                    deleteApplication={jest.fn()}
 | 
			
		||||
                    history={{}}
 | 
			
		||||
                    locationSettings={{ locale: 'en-GB' }}
 | 
			
		||||
                />
 | 
			
		||||
            </AccessProvider>
 | 
			
		||||
        )
 | 
			
		||||
@ -77,7 +78,7 @@ test('renders correctly without permission', () => {
 | 
			
		||||
                                url: 'http://example.org',
 | 
			
		||||
                                description: 'app description',
 | 
			
		||||
                            }}
 | 
			
		||||
                            location={{ locale: 'en-GB' }}
 | 
			
		||||
                            locationSettings={{ locale: 'en-GB' }}
 | 
			
		||||
                        />
 | 
			
		||||
                    </AccessProvider>
 | 
			
		||||
                </ThemeProvider>
 | 
			
		||||
@ -140,7 +141,7 @@ test('renders correctly with permissions', () => {
 | 
			
		||||
                                url: 'http://example.org',
 | 
			
		||||
                                description: 'app description',
 | 
			
		||||
                            }}
 | 
			
		||||
                            location={{ locale: 'en-GB' }}
 | 
			
		||||
                            locationSettings={{ locale: 'en-GB' }}
 | 
			
		||||
                        />
 | 
			
		||||
                    </AccessProvider>
 | 
			
		||||
                </ThemeProvider>
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,7 @@ class ClientApplications extends PureComponent {
 | 
			
		||||
        fetchApplication: PropTypes.func.isRequired,
 | 
			
		||||
        appName: PropTypes.string,
 | 
			
		||||
        application: PropTypes.object,
 | 
			
		||||
        location: PropTypes.object,
 | 
			
		||||
        locationSettings: PropTypes.object.isRequired,
 | 
			
		||||
        storeApplicationMetaData: PropTypes.func.isRequired,
 | 
			
		||||
        deleteApplication: PropTypes.func.isRequired,
 | 
			
		||||
        history: PropTypes.object.isRequired,
 | 
			
		||||
@ -54,8 +54,8 @@ class ClientApplications extends PureComponent {
 | 
			
		||||
            .finally(() => this.setState({ loading: false }));
 | 
			
		||||
    }
 | 
			
		||||
    formatFullDateTime = v =>
 | 
			
		||||
        formatFullDateTimeWithLocale(v, this.props.location.locale);
 | 
			
		||||
    formatDate = v => formatDateWithLocale(v, this.props.location.locale);
 | 
			
		||||
        formatFullDateTimeWithLocale(v, this.props.locationSettings.locale);
 | 
			
		||||
    formatDate = v => formatDateWithLocale(v, this.props.locationSettings.locale);
 | 
			
		||||
 | 
			
		||||
    deleteApplication = async evt => {
 | 
			
		||||
        evt.preventDefault();
 | 
			
		||||
 | 
			
		||||
@ -1,23 +0,0 @@
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import ApplicationEdit from './application-edit-component';
 | 
			
		||||
import { fetchApplication, storeApplicationMetaData, deleteApplication } from './../../store/application/actions';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = (state, props) => {
 | 
			
		||||
    let application = state.applications.getIn(['apps', props.appName]);
 | 
			
		||||
    const location = state.settings.toJS().location || {};
 | 
			
		||||
    if (application) {
 | 
			
		||||
        application = application.toJS();
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
        application,
 | 
			
		||||
        location,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Container = connect(mapStateToProps, {
 | 
			
		||||
    fetchApplication,
 | 
			
		||||
    storeApplicationMetaData,
 | 
			
		||||
    deleteApplication,
 | 
			
		||||
})(ApplicationEdit);
 | 
			
		||||
 | 
			
		||||
export default Container;
 | 
			
		||||
@ -0,0 +1,30 @@
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import ApplicationEdit from './application-edit-component';
 | 
			
		||||
import {
 | 
			
		||||
    deleteApplication,
 | 
			
		||||
    fetchApplication,
 | 
			
		||||
    storeApplicationMetaData,
 | 
			
		||||
} from '../../store/application/actions';
 | 
			
		||||
import { useLocationSettings } from '../../hooks/useLocationSettings';
 | 
			
		||||
 | 
			
		||||
const ApplicationEditContainer = props => {
 | 
			
		||||
    const { locationSettings } = useLocationSettings();
 | 
			
		||||
 | 
			
		||||
    return <ApplicationEdit {...props} locationSettings={locationSettings} />;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = (state, props) => {
 | 
			
		||||
    let application = state.applications.getIn(['apps', props.appName]);
 | 
			
		||||
    if (application) {
 | 
			
		||||
        application = application.toJS();
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
        application,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps, {
 | 
			
		||||
    fetchApplication,
 | 
			
		||||
    storeApplicationMetaData,
 | 
			
		||||
    deleteApplication,
 | 
			
		||||
})(ApplicationEditContainer);
 | 
			
		||||
							
								
								
									
										36
									
								
								frontend/src/component/archive/ArchiveListContainer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								frontend/src/component/archive/ArchiveListContainer.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
			
		||||
import { useFeaturesArchive } from '../../hooks/api/getters/useFeaturesArchive/useFeaturesArchive';
 | 
			
		||||
import FeatureToggleList from '../feature/FeatureToggleList/FeatureToggleList';
 | 
			
		||||
import useUiConfig from '../../hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import { useFeaturesFilter } from '../../hooks/useFeaturesFilter';
 | 
			
		||||
import { useFeatureArchiveApi } from '../../hooks/api/actions/useFeatureArchiveApi/useReviveFeatureApi';
 | 
			
		||||
import useToast from '../../hooks/useToast';
 | 
			
		||||
import { useFeaturesSort } from '../../hooks/useFeaturesSort';
 | 
			
		||||
 | 
			
		||||
export const ArchiveListContainer = () => {
 | 
			
		||||
    const { setToastApiError } = useToast();
 | 
			
		||||
    const { uiConfig } = useUiConfig();
 | 
			
		||||
    const { reviveFeature } = useFeatureArchiveApi();
 | 
			
		||||
    const { archivedFeatures, loading, refetchArchived } = useFeaturesArchive();
 | 
			
		||||
    const { filtered, filter, setFilter } = useFeaturesFilter(archivedFeatures);
 | 
			
		||||
    const { sorted, sort, setSort } = useFeaturesSort(filtered);
 | 
			
		||||
 | 
			
		||||
    const revive = (feature: string) => {
 | 
			
		||||
        reviveFeature(feature)
 | 
			
		||||
            .then(refetchArchived)
 | 
			
		||||
            .catch(e => setToastApiError(e.toString()));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <FeatureToggleList
 | 
			
		||||
            features={sorted}
 | 
			
		||||
            loading={loading}
 | 
			
		||||
            revive={revive}
 | 
			
		||||
            flags={uiConfig.flags}
 | 
			
		||||
            filter={filter}
 | 
			
		||||
            setFilter={setFilter}
 | 
			
		||||
            sort={sort}
 | 
			
		||||
            setSort={setSort}
 | 
			
		||||
            archive
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -1,19 +0,0 @@
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import FeatureListComponent from '../feature/FeatureToggleList/FeatureToggleList';
 | 
			
		||||
import { fetchArchive, revive } from './../../store/archive/actions';
 | 
			
		||||
import { updateSettingForGroup } from './../../store/settings/actions';
 | 
			
		||||
import { mapStateToPropsConfigurable } from '../feature/FeatureToggleList';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = mapStateToPropsConfigurable(false);
 | 
			
		||||
const mapDispatchToProps = {
 | 
			
		||||
    fetcher: () => fetchArchive(),
 | 
			
		||||
    revive,
 | 
			
		||||
    updateSetting: updateSettingForGroup('feature'),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ArchiveListContainer = connect(
 | 
			
		||||
    mapStateToProps,
 | 
			
		||||
    mapDispatchToProps
 | 
			
		||||
)(FeatureListComponent);
 | 
			
		||||
 | 
			
		||||
export default ArchiveListContainer;
 | 
			
		||||
@ -1,37 +0,0 @@
 | 
			
		||||
.archiveList {
 | 
			
		||||
    background-color: #fff;
 | 
			
		||||
    color: rgba(0, 0, 0, 0.54);
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    padding: 0 16px 0 18px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.listItemToggle {
 | 
			
		||||
    width: 40%;
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
    margin-right: 20%;
 | 
			
		||||
}
 | 
			
		||||
.listItemCreated {
 | 
			
		||||
    width: 10%;
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
    margin-right: 2px;
 | 
			
		||||
}
 | 
			
		||||
.listItemRevive {
 | 
			
		||||
    width: 5%;
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
    margin-right: 10%;
 | 
			
		||||
}
 | 
			
		||||
.toggleDetails {
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
    font-weight: 400;
 | 
			
		||||
    line-height: 24px;
 | 
			
		||||
    letter-spacing: 0;
 | 
			
		||||
    line-height: 18px;
 | 
			
		||||
    color: rgba(0, 0, 0, 0.54);
 | 
			
		||||
    display: block;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
}
 | 
			
		||||
.strategiesList {
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
    float: right;
 | 
			
		||||
    margin-left: 8px !important;
 | 
			
		||||
}
 | 
			
		||||
@ -13,16 +13,21 @@ interface IPermissionSwitchProps extends OverridableComponent<any> {
 | 
			
		||||
    checked: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const PermissionSwitch: React.FC<IPermissionSwitchProps> = ({
 | 
			
		||||
    permission,
 | 
			
		||||
    tooltip = '',
 | 
			
		||||
    disabled,
 | 
			
		||||
    projectId,
 | 
			
		||||
    environmentId,
 | 
			
		||||
    checked,
 | 
			
		||||
    onChange,
 | 
			
		||||
    ...rest
 | 
			
		||||
}) => {
 | 
			
		||||
const PermissionSwitch = React.forwardRef<
 | 
			
		||||
    HTMLButtonElement,
 | 
			
		||||
    IPermissionSwitchProps
 | 
			
		||||
>((props, ref) => {
 | 
			
		||||
    const {
 | 
			
		||||
        permission,
 | 
			
		||||
        tooltip = '',
 | 
			
		||||
        disabled,
 | 
			
		||||
        projectId,
 | 
			
		||||
        environmentId,
 | 
			
		||||
        checked,
 | 
			
		||||
        onChange,
 | 
			
		||||
        ...rest
 | 
			
		||||
    } = props;
 | 
			
		||||
 | 
			
		||||
    const { hasAccess } = useContext(AccessContext);
 | 
			
		||||
 | 
			
		||||
    let access;
 | 
			
		||||
@ -45,11 +50,12 @@ const PermissionSwitch: React.FC<IPermissionSwitchProps> = ({
 | 
			
		||||
                    onChange={onChange}
 | 
			
		||||
                    disabled={disabled || !access}
 | 
			
		||||
                    checked={checked}
 | 
			
		||||
                    ref={ref}
 | 
			
		||||
                    {...rest}
 | 
			
		||||
                />
 | 
			
		||||
            </span>
 | 
			
		||||
        </Tooltip>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default PermissionSwitch;
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import React, { useEffect } from 'react';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { MenuItem } from '@material-ui/core';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import DropdownMenu from '../DropdownMenu/DropdownMenu';
 | 
			
		||||
@ -9,20 +9,8 @@ const ALL_PROJECTS = { id: '*', name: '> All projects' };
 | 
			
		||||
const ProjectSelect = ({ currentProjectId, updateCurrentProject, ...rest }) => {
 | 
			
		||||
    const { projects } = useProjects();
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        let currentProject = projects.find(i => i.id === currentProjectId);
 | 
			
		||||
 | 
			
		||||
        if (currentProject) {
 | 
			
		||||
            setProject(currentProject.id);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setProject('*');
 | 
			
		||||
        /* eslint-disable-next-line */
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const setProject = v => {
 | 
			
		||||
        const id = typeof v === 'string' ? v.trim() : '';
 | 
			
		||||
        const id = v && typeof v === 'string' ? v.trim() : '*';
 | 
			
		||||
        updateCurrentProject(id);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,9 +3,8 @@ import ProjectSelect from './ProjectSelect';
 | 
			
		||||
import { fetchProjects } from '../../../store/project/actions';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = (state, ownProps) => ({
 | 
			
		||||
    ...ownProps,
 | 
			
		||||
    projects: state.projects.toJS(),
 | 
			
		||||
    currentProjectId: ownProps.settings.currentProjectId || '*',
 | 
			
		||||
    updateCurrentProject: id => ownProps.updateSetting('currentProjectId', id),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps, { fetchProjects })(ProjectSelect);
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,7 @@ const TabNav = ({
 | 
			
		||||
 | 
			
		||||
TabNav.propTypes = {
 | 
			
		||||
    tabData: PropTypes.array.isRequired,
 | 
			
		||||
    navClass: PropTypes.string,
 | 
			
		||||
    className: PropTypes.string,
 | 
			
		||||
    startingTab: PropTypes.number,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -79,7 +79,7 @@ const CreateFeature = () => {
 | 
			
		||||
            title="Create Feature toggle"
 | 
			
		||||
            description="Feature toggles support different use cases, each with their own specific needs such as simple static routing or more complex routing.
 | 
			
		||||
            The feature toggle is disabled when created and you decide when to enable"
 | 
			
		||||
            documentationLink="https://docs.getunleash.io/"
 | 
			
		||||
            documentationLink="https://docs.getunleash.io/advanced/feature_toggle_types"
 | 
			
		||||
            formatApiCode={formatApiCode}
 | 
			
		||||
        >
 | 
			
		||||
            <FeatureForm
 | 
			
		||||
 | 
			
		||||
@ -83,7 +83,7 @@ const EditFeature = () => {
 | 
			
		||||
            title="Edit Feature toggle"
 | 
			
		||||
            description="Feature toggles support different use cases, each with their own specific needs such as simple static routing or more complex routing.
 | 
			
		||||
            The feature toggle is disabled when created and you decide when to enable"
 | 
			
		||||
            documentationLink="https://docs.getunleash.io/"
 | 
			
		||||
            documentationLink="https://docs.getunleash.io/advanced/feature_toggle_types"
 | 
			
		||||
            formatApiCode={formatApiCode}
 | 
			
		||||
        >
 | 
			
		||||
            <FeatureForm
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@ import ConditionallyRender from '../../common/ConditionallyRender';
 | 
			
		||||
import { trim } from '../../common/util';
 | 
			
		||||
import Input from '../../common/Input/Input';
 | 
			
		||||
import { CREATE_FEATURE } from '../../providers/AccessProvider/permissions';
 | 
			
		||||
import { useHistory } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
interface IFeatureToggleForm {
 | 
			
		||||
    type: string;
 | 
			
		||||
@ -22,8 +23,8 @@ interface IFeatureToggleForm {
 | 
			
		||||
    setName: React.Dispatch<React.SetStateAction<string>>;
 | 
			
		||||
    setDescription: React.Dispatch<React.SetStateAction<string>>;
 | 
			
		||||
    setProject: React.Dispatch<React.SetStateAction<string>>;
 | 
			
		||||
    validateToggleName: () => void;
 | 
			
		||||
    setImpressionData: React.Dispatch<React.SetStateAction<boolean>>;
 | 
			
		||||
    validateToggleName?: () => void;
 | 
			
		||||
    handleSubmit: (e: any) => void;
 | 
			
		||||
    handleCancel: () => void;
 | 
			
		||||
    errors: { [key: string]: string };
 | 
			
		||||
@ -52,6 +53,7 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
 | 
			
		||||
}) => {
 | 
			
		||||
    const styles = useStyles();
 | 
			
		||||
    const { featureTypes } = useFeatureTypes();
 | 
			
		||||
    const history = useHistory();
 | 
			
		||||
    const { permissions } = useUser();
 | 
			
		||||
    const editable = mode !== 'Edit';
 | 
			
		||||
 | 
			
		||||
@ -75,9 +77,7 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
 | 
			
		||||
                    onFocus={() => clearErrors()}
 | 
			
		||||
                    value={name}
 | 
			
		||||
                    onChange={e => setName(trim(e.target.value))}
 | 
			
		||||
                    inputProps={{
 | 
			
		||||
                        'data-test': CF_NAME_ID,
 | 
			
		||||
                    }}
 | 
			
		||||
                    data-test={CF_NAME_ID}
 | 
			
		||||
                    onBlur={validateToggleName}
 | 
			
		||||
                />
 | 
			
		||||
                <p className={styles.inputDescription}>
 | 
			
		||||
@ -89,9 +89,7 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
 | 
			
		||||
                    label={'Toggle type'}
 | 
			
		||||
                    id="feature-type-select"
 | 
			
		||||
                    editable
 | 
			
		||||
                    inputProps={{
 | 
			
		||||
                        'data-test': CF_TYPE_ID,
 | 
			
		||||
                    }}
 | 
			
		||||
                    data-test={CF_TYPE_ID}
 | 
			
		||||
                    IconComponent={KeyboardArrowDownOutlined}
 | 
			
		||||
                    className={styles.selectInput}
 | 
			
		||||
                />
 | 
			
		||||
@ -108,7 +106,12 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
 | 
			
		||||
                />
 | 
			
		||||
                <FeatureProjectSelect
 | 
			
		||||
                    value={project}
 | 
			
		||||
                    onChange={e => setProject(e.target.value)}
 | 
			
		||||
                    onChange={e => {
 | 
			
		||||
                        setProject(e.target.value);
 | 
			
		||||
                        history.replace(
 | 
			
		||||
                            `/projects/${e.target.value}/create-toggle`
 | 
			
		||||
                        );
 | 
			
		||||
                    }}
 | 
			
		||||
                    enabled={editable}
 | 
			
		||||
                    filter={projectFilterGenerator(
 | 
			
		||||
                        { permissions },
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { useContext, useLayoutEffect, useEffect } from 'react';
 | 
			
		||||
import { useContext } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import classnames from 'classnames';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import { Button, List, Tooltip, IconButton, ListItem } from '@material-ui/core';
 | 
			
		||||
import { Button, IconButton, List, ListItem, Tooltip } from '@material-ui/core';
 | 
			
		||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
 | 
			
		||||
import { Add } from '@material-ui/icons';
 | 
			
		||||
 | 
			
		||||
@ -23,43 +23,31 @@ import { useStyles } from './styles';
 | 
			
		||||
import ListPlaceholder from '../../common/ListPlaceholder/ListPlaceholder';
 | 
			
		||||
import { getCreateTogglePath } from '../../../utils/route-path-helpers';
 | 
			
		||||
import { NAVIGATE_TO_CREATE_FEATURE } from '../../../testIds';
 | 
			
		||||
import { resolveFilteredProjectId } from '../../../hooks/useFeaturesFilter';
 | 
			
		||||
 | 
			
		||||
const FeatureToggleList = ({
 | 
			
		||||
    fetcher,
 | 
			
		||||
    features,
 | 
			
		||||
    settings,
 | 
			
		||||
    revive,
 | 
			
		||||
    currentProjectId,
 | 
			
		||||
    updateSetting,
 | 
			
		||||
    featureMetrics,
 | 
			
		||||
    toggleFeature,
 | 
			
		||||
    archive,
 | 
			
		||||
    loading,
 | 
			
		||||
    flags,
 | 
			
		||||
    filter,
 | 
			
		||||
    setFilter,
 | 
			
		||||
    sort,
 | 
			
		||||
    setSort,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { hasAccess } = useContext(AccessContext);
 | 
			
		||||
    const styles = useStyles();
 | 
			
		||||
    const smallScreen = useMediaQuery('(max-width:800px)');
 | 
			
		||||
    const mobileView = useMediaQuery('(max-width:600px)');
 | 
			
		||||
 | 
			
		||||
    useLayoutEffect(() => {
 | 
			
		||||
        fetcher();
 | 
			
		||||
    }, [fetcher]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        updateSetting('filter', '');
 | 
			
		||||
        /* eslint-disable-next-line */
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const toggleMetrics = () => {
 | 
			
		||||
        updateSetting('showLastHour', !settings.showLastHour);
 | 
			
		||||
    const setFilterQuery = v => {
 | 
			
		||||
        const query = v && typeof v === 'string' ? v.trim() : '';
 | 
			
		||||
        setFilter(prev => ({ ...prev, query }));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const setSort = v => {
 | 
			
		||||
        updateSetting('sort', typeof v === 'string' ? v.trim() : '');
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const createURL = getCreateTogglePath(currentProjectId, flags.E);
 | 
			
		||||
    const resolvedProjectId = resolveFilteredProjectId(filter);
 | 
			
		||||
    const createURL = getCreateTogglePath(resolvedProjectId, flags.E);
 | 
			
		||||
 | 
			
		||||
    const renderFeatures = () => {
 | 
			
		||||
        features.forEach(e => {
 | 
			
		||||
@ -70,11 +58,7 @@ const FeatureToggleList = ({
 | 
			
		||||
            return loadingFeatures.map(feature => (
 | 
			
		||||
                <FeatureToggleListItem
 | 
			
		||||
                    key={feature.name}
 | 
			
		||||
                    settings={settings}
 | 
			
		||||
                    metricsLastHour={featureMetrics.lastHour[feature.name]}
 | 
			
		||||
                    metricsLastMinute={featureMetrics.lastMinute[feature.name]}
 | 
			
		||||
                    feature={feature}
 | 
			
		||||
                    toggleFeature={toggleFeature}
 | 
			
		||||
                    revive={revive}
 | 
			
		||||
                    hasAccess={hasAccess}
 | 
			
		||||
                    className={'skeleton'}
 | 
			
		||||
@ -89,13 +73,7 @@ const FeatureToggleList = ({
 | 
			
		||||
                show={features.map(feature => (
 | 
			
		||||
                    <FeatureToggleListItem
 | 
			
		||||
                        key={feature.name}
 | 
			
		||||
                        settings={settings}
 | 
			
		||||
                        metricsLastHour={featureMetrics.lastHour[feature.name]}
 | 
			
		||||
                        metricsLastMinute={
 | 
			
		||||
                            featureMetrics.lastMinute[feature.name]
 | 
			
		||||
                        }
 | 
			
		||||
                        feature={feature}
 | 
			
		||||
                        toggleFeature={toggleFeature}
 | 
			
		||||
                        revive={revive}
 | 
			
		||||
                        hasAccess={hasAccess}
 | 
			
		||||
                        flags={flags}
 | 
			
		||||
@ -129,7 +107,7 @@ const FeatureToggleList = ({
 | 
			
		||||
        <div className={styles.featureContainer}>
 | 
			
		||||
            <div className={styles.searchBarContainer}>
 | 
			
		||||
                <SearchField
 | 
			
		||||
                    updateValue={updateSetting.bind(this, 'filter')}
 | 
			
		||||
                    updateValue={setFilterQuery}
 | 
			
		||||
                    className={classnames(styles.searchBar, {
 | 
			
		||||
                        skeleton: loading,
 | 
			
		||||
                    })}
 | 
			
		||||
@ -151,10 +129,10 @@ const FeatureToggleList = ({
 | 
			
		||||
                                    condition={!smallScreen}
 | 
			
		||||
                                    show={
 | 
			
		||||
                                        <FeatureToggleListActions
 | 
			
		||||
                                            settings={settings}
 | 
			
		||||
                                            toggleMetrics={toggleMetrics}
 | 
			
		||||
                                            filter={filter}
 | 
			
		||||
                                            setFilter={setFilter}
 | 
			
		||||
                                            sort={sort}
 | 
			
		||||
                                            setSort={setSort}
 | 
			
		||||
                                            updateSetting={updateSetting}
 | 
			
		||||
                                            loading={loading}
 | 
			
		||||
                                        />
 | 
			
		||||
                                    }
 | 
			
		||||
@ -175,7 +153,7 @@ const FeatureToggleList = ({
 | 
			
		||||
                                                        disabled={
 | 
			
		||||
                                                            !hasAccess(
 | 
			
		||||
                                                                CREATE_FEATURE,
 | 
			
		||||
                                                                currentProjectId
 | 
			
		||||
                                                                resolvedProjectId
 | 
			
		||||
                                                            )
 | 
			
		||||
                                                        }
 | 
			
		||||
                                                    >
 | 
			
		||||
@ -195,7 +173,7 @@ const FeatureToggleList = ({
 | 
			
		||||
                                                    disabled={
 | 
			
		||||
                                                        !hasAccess(
 | 
			
		||||
                                                            CREATE_FEATURE,
 | 
			
		||||
                                                            currentProjectId
 | 
			
		||||
                                                            resolvedProjectId
 | 
			
		||||
                                                        )
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                    className={classnames({
 | 
			
		||||
@ -221,16 +199,14 @@ const FeatureToggleList = ({
 | 
			
		||||
 | 
			
		||||
FeatureToggleList.propTypes = {
 | 
			
		||||
    features: PropTypes.array.isRequired,
 | 
			
		||||
    featureMetrics: PropTypes.object.isRequired,
 | 
			
		||||
    fetcher: PropTypes.func,
 | 
			
		||||
    revive: PropTypes.func,
 | 
			
		||||
    updateSetting: PropTypes.func.isRequired,
 | 
			
		||||
    toggleFeature: PropTypes.func,
 | 
			
		||||
    settings: PropTypes.object,
 | 
			
		||||
    history: PropTypes.object.isRequired,
 | 
			
		||||
    loading: PropTypes.bool,
 | 
			
		||||
    currentProjectId: PropTypes.string.isRequired,
 | 
			
		||||
    archive: PropTypes.bool,
 | 
			
		||||
    flags: PropTypes.object,
 | 
			
		||||
    filter: PropTypes.object.isRequired,
 | 
			
		||||
    setFilter: PropTypes.func.isRequired,
 | 
			
		||||
    sort: PropTypes.object.isRequired,
 | 
			
		||||
    setSort: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default FeatureToggleList;
 | 
			
		||||
 | 
			
		||||
@ -2,31 +2,21 @@ import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import { MenuItem, Typography } from '@material-ui/core';
 | 
			
		||||
// import { HourglassEmpty, HourglassFull } from '@material-ui/icons';
 | 
			
		||||
// import { MenuItemWithIcon } from '../../../common';
 | 
			
		||||
import DropdownMenu from '../../../common/DropdownMenu/DropdownMenu';
 | 
			
		||||
import ProjectSelect from '../../../common/ProjectSelect';
 | 
			
		||||
import { useStyles } from './styles';
 | 
			
		||||
import useLoading from '../../../../hooks/useLoading';
 | 
			
		||||
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import ConditionallyRender from '../../../common/ConditionallyRender';
 | 
			
		||||
import { createFeaturesFilterSortOptions } from '../../../../hooks/useFeaturesSort';
 | 
			
		||||
 | 
			
		||||
const sortingOptions = [
 | 
			
		||||
    { type: 'name', displayName: 'Name' },
 | 
			
		||||
    { type: 'type', displayName: 'Type' },
 | 
			
		||||
    { type: 'enabled', displayName: 'Enabled' },
 | 
			
		||||
    { type: 'stale', displayName: 'Stale' },
 | 
			
		||||
    { type: 'created', displayName: 'Created' },
 | 
			
		||||
    { type: 'Last seen', displayName: 'Last seen' },
 | 
			
		||||
    { type: 'project', displayName: 'Project' },
 | 
			
		||||
    { type: 'metrics', displayName: 'Metrics' },
 | 
			
		||||
];
 | 
			
		||||
const sortOptions = createFeaturesFilterSortOptions();
 | 
			
		||||
 | 
			
		||||
const FeatureToggleListActions = ({
 | 
			
		||||
    settings,
 | 
			
		||||
    filter,
 | 
			
		||||
    setFilter,
 | 
			
		||||
    sort,
 | 
			
		||||
    setSort,
 | 
			
		||||
    toggleMetrics,
 | 
			
		||||
    updateSetting,
 | 
			
		||||
    loading,
 | 
			
		||||
}) => {
 | 
			
		||||
    const styles = useStyles();
 | 
			
		||||
@ -34,65 +24,33 @@ const FeatureToggleListActions = ({
 | 
			
		||||
    const ref = useLoading(loading);
 | 
			
		||||
 | 
			
		||||
    const handleSort = e => {
 | 
			
		||||
        const target = e.target.getAttribute('data-target');
 | 
			
		||||
        setSort(target);
 | 
			
		||||
        const type = e.target.getAttribute('data-target')?.trim();
 | 
			
		||||
        type && setSort(prev => ({ ...prev, type }));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const isDisabled = type => settings.sort === type;
 | 
			
		||||
    const isDisabled = s => s === sort.type;
 | 
			
		||||
    const selectedOption = sortOptions.find(o => o.type === sort.type) || sortOptions[0];
 | 
			
		||||
 | 
			
		||||
    const renderSortingOptions = () =>
 | 
			
		||||
        sortingOptions.map(option => (
 | 
			
		||||
        sortOptions.map(option => (
 | 
			
		||||
            <MenuItem
 | 
			
		||||
                style={{ fontSize: '14px' }}
 | 
			
		||||
                key={option.type}
 | 
			
		||||
                disabled={isDisabled(option.type)}
 | 
			
		||||
                data-target={option.type}
 | 
			
		||||
            >
 | 
			
		||||
                {option.displayName}
 | 
			
		||||
                {option.name}
 | 
			
		||||
            </MenuItem>
 | 
			
		||||
        ));
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
    const renderMetricsOptions = () => [
 | 
			
		||||
        <MenuItemWithIcon
 | 
			
		||||
            style={{ fontSize: '14px' }}
 | 
			
		||||
            icon={HourglassEmpty}
 | 
			
		||||
            disabled={!settings.showLastHour}
 | 
			
		||||
            data-target="minute"
 | 
			
		||||
            label="Last minute"
 | 
			
		||||
            key={1}
 | 
			
		||||
        />,
 | 
			
		||||
        <MenuItemWithIcon
 | 
			
		||||
            style={{ fontSize: '14px' }}
 | 
			
		||||
            icon={HourglassFull}
 | 
			
		||||
            disabled={settings.showLastHour}
 | 
			
		||||
            data-target="hour"
 | 
			
		||||
            label="Last hour"
 | 
			
		||||
            key={2}
 | 
			
		||||
        />,
 | 
			
		||||
    ];
 | 
			
		||||
    */
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className={styles.actions} ref={ref}>
 | 
			
		||||
            <Typography variant="body2" data-loading>
 | 
			
		||||
                Sorted by:
 | 
			
		||||
            </Typography>
 | 
			
		||||
            {/* }
 | 
			
		||||
            <DropdownMenu
 | 
			
		||||
                id={'metric'}
 | 
			
		||||
                label={`Last ${settings.showLastHour ? 'hour' : 'minute'}`}
 | 
			
		||||
                title="Metric interval"
 | 
			
		||||
                callback={toggleMetrics}
 | 
			
		||||
                renderOptions={renderMetricsOptions}
 | 
			
		||||
                className=""
 | 
			
		||||
                style={{ textTransform: 'lowercase', fontWeight: 'normal' }}
 | 
			
		||||
                data-loading
 | 
			
		||||
            />
 | 
			
		||||
            {*/}
 | 
			
		||||
            <DropdownMenu
 | 
			
		||||
                id={'sorting'}
 | 
			
		||||
                label={`By ${settings.sort}`}
 | 
			
		||||
                label={`By ${selectedOption.name}`}
 | 
			
		||||
                callback={handleSort}
 | 
			
		||||
                renderOptions={renderSortingOptions}
 | 
			
		||||
                title="Sort by"
 | 
			
		||||
@ -104,8 +62,8 @@ const FeatureToggleListActions = ({
 | 
			
		||||
                condition={uiConfig.flags.P}
 | 
			
		||||
                show={
 | 
			
		||||
                    <ProjectSelect
 | 
			
		||||
                        settings={settings}
 | 
			
		||||
                        updateSetting={updateSetting}
 | 
			
		||||
                        currentProjectId={filter.project}
 | 
			
		||||
                        updateCurrentProject={project => setFilter(prev => ({ ...prev, project }))}
 | 
			
		||||
                        style={{
 | 
			
		||||
                            textTransform: 'lowercase',
 | 
			
		||||
                            fontWeight: 'normal',
 | 
			
		||||
@ -119,10 +77,11 @@ const FeatureToggleListActions = ({
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
FeatureToggleListActions.propTypes = {
 | 
			
		||||
    settings: PropTypes.object,
 | 
			
		||||
    filter: PropTypes.object,
 | 
			
		||||
    setFilter: PropTypes.func,
 | 
			
		||||
    sort: PropTypes.object,
 | 
			
		||||
    setSort: PropTypes.func,
 | 
			
		||||
    toggleMetrics: PropTypes.func,
 | 
			
		||||
    updateSetting: PropTypes.func,
 | 
			
		||||
    loading: PropTypes.bool,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,24 @@
 | 
			
		||||
import { useFeatures } from '../../../hooks/api/getters/useFeatures/useFeatures';
 | 
			
		||||
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import { useFeaturesFilter } from '../../../hooks/useFeaturesFilter';
 | 
			
		||||
import FeatureToggleList from './FeatureToggleList';
 | 
			
		||||
import { useFeaturesSort } from '../../../hooks/useFeaturesSort';
 | 
			
		||||
 | 
			
		||||
export const FeatureToggleListContainer = () => {
 | 
			
		||||
    const { uiConfig } = useUiConfig();
 | 
			
		||||
    const { features, loading } = useFeatures();
 | 
			
		||||
    const { filtered, filter, setFilter } = useFeaturesFilter(features);
 | 
			
		||||
    const { sorted, sort, setSort } = useFeaturesSort(filtered);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <FeatureToggleList
 | 
			
		||||
            features={sorted}
 | 
			
		||||
            loading={loading}
 | 
			
		||||
            flags={uiConfig.flags}
 | 
			
		||||
            filter={filter}
 | 
			
		||||
            setFilter={setFilter}
 | 
			
		||||
            sort={sort}
 | 
			
		||||
            setSort={setSort}
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -22,10 +22,6 @@ import PermissionIconButton from '../../../common/PermissionIconButton/Permissio
 | 
			
		||||
 | 
			
		||||
const FeatureToggleListItem = ({
 | 
			
		||||
    feature,
 | 
			
		||||
    toggleFeature,
 | 
			
		||||
    settings,
 | 
			
		||||
    metricsLastHour = { yes: 0, no: 0, isFallback: true },
 | 
			
		||||
    metricsLastMinute = { yes: 0, no: 0, isFallback: true },
 | 
			
		||||
    revive,
 | 
			
		||||
    hasAccess,
 | 
			
		||||
    flags = {},
 | 
			
		||||
@ -164,10 +160,6 @@ const FeatureToggleListItem = ({
 | 
			
		||||
 | 
			
		||||
FeatureToggleListItem.propTypes = {
 | 
			
		||||
    feature: PropTypes.object,
 | 
			
		||||
    toggleFeature: PropTypes.func,
 | 
			
		||||
    settings: PropTypes.object,
 | 
			
		||||
    metricsLastHour: PropTypes.object,
 | 
			
		||||
    metricsLastMinute: PropTypes.object,
 | 
			
		||||
    revive: PropTypes.func,
 | 
			
		||||
    hasAccess: PropTypes.func.isRequired,
 | 
			
		||||
    flags: PropTypes.object,
 | 
			
		||||
 | 
			
		||||
@ -1,26 +0,0 @@
 | 
			
		||||
import React, { memo } from 'react';
 | 
			
		||||
import { Chip } from '@material-ui/core';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import { useStyles } from './styles';
 | 
			
		||||
 | 
			
		||||
const FeatureToggleListItemChip = ({ type, types, onClick }) => {
 | 
			
		||||
    const styles = useStyles();
 | 
			
		||||
 | 
			
		||||
    const typeObject = types.find(o => o.id === type) || {
 | 
			
		||||
        id: type,
 | 
			
		||||
        name: type,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Chip className={styles.typeChip} title={typeObject.description} label={typeObject.name} onClick={onClick} />
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
FeatureToggleListItemChip.propTypes = {
 | 
			
		||||
    type: PropTypes.string.isRequired,
 | 
			
		||||
    types: PropTypes.array,
 | 
			
		||||
    onClick: PropTypes.func,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default memo(FeatureToggleListItemChip);
 | 
			
		||||
@ -1,10 +0,0 @@
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import Component from './FeatureToggleListItemChip';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => ({
 | 
			
		||||
    types: state.featureTypes.toJS(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const FeatureType = connect(mapStateToProps)(Component);
 | 
			
		||||
 | 
			
		||||
export default FeatureType;
 | 
			
		||||
@ -1,10 +0,0 @@
 | 
			
		||||
import { makeStyles } from '@material-ui/styles';
 | 
			
		||||
 | 
			
		||||
export const useStyles = makeStyles(theme => ({
 | 
			
		||||
    typeChip: {
 | 
			
		||||
        margin: '0 8px',
 | 
			
		||||
        background: 'transparent',
 | 
			
		||||
        border: `1px solid ${theme.palette.primary.main}`,
 | 
			
		||||
        color: theme.palette.primary.main,
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
@ -117,7 +117,7 @@ exports[`renders correctly with one feature 1`] = `
 | 
			
		||||
                <span
 | 
			
		||||
                  className="MuiButton-label"
 | 
			
		||||
                >
 | 
			
		||||
                  By name
 | 
			
		||||
                  By Name
 | 
			
		||||
                  <span
 | 
			
		||||
                    className="MuiButton-endIcon MuiButton-iconSizeMedium"
 | 
			
		||||
                  >
 | 
			
		||||
@ -185,12 +185,6 @@ exports[`renders correctly with one feature 1`] = `
 | 
			
		||||
          }
 | 
			
		||||
          flags={Object {}}
 | 
			
		||||
          hasAccess={[Function]}
 | 
			
		||||
          settings={
 | 
			
		||||
            Object {
 | 
			
		||||
              "sort": "name",
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          toggleFeature={[MockFunction]}
 | 
			
		||||
        />
 | 
			
		||||
      </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
@ -315,7 +309,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
 | 
			
		||||
                <span
 | 
			
		||||
                  className="MuiButton-label"
 | 
			
		||||
                >
 | 
			
		||||
                  By name
 | 
			
		||||
                  By Name
 | 
			
		||||
                  <span
 | 
			
		||||
                    className="MuiButton-endIcon MuiButton-iconSizeMedium"
 | 
			
		||||
                  >
 | 
			
		||||
@ -386,12 +380,6 @@ exports[`renders correctly with one feature without permissions 1`] = `
 | 
			
		||||
          }
 | 
			
		||||
          flags={Object {}}
 | 
			
		||||
          hasAccess={[Function]}
 | 
			
		||||
          settings={
 | 
			
		||||
            Object {
 | 
			
		||||
              "sort": "name",
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          toggleFeature={[MockFunction]}
 | 
			
		||||
        />
 | 
			
		||||
      </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -7,8 +7,6 @@ import renderer from 'react-test-renderer';
 | 
			
		||||
 | 
			
		||||
import theme from '../../../../themes/main-theme';
 | 
			
		||||
 | 
			
		||||
jest.mock('../FeatureToggleListItem/FeatureToggleListItemChip');
 | 
			
		||||
 | 
			
		||||
test('renders correctly with one feature', () => {
 | 
			
		||||
    const feature = {
 | 
			
		||||
        name: 'Another',
 | 
			
		||||
@ -26,18 +24,12 @@ test('renders correctly with one feature', () => {
 | 
			
		||||
        ],
 | 
			
		||||
        createdAt: '2018-02-04T20:27:52.127Z',
 | 
			
		||||
    };
 | 
			
		||||
    const featureMetrics = { lastHour: {}, lastMinute: {}, seenApps: {} };
 | 
			
		||||
    const settings = { sort: 'name' };
 | 
			
		||||
    const tree = renderer.create(
 | 
			
		||||
        <MemoryRouter>
 | 
			
		||||
            <ThemeProvider theme={theme}>
 | 
			
		||||
                <FeatureToggleListItem
 | 
			
		||||
                    key={0}
 | 
			
		||||
                    settings={settings}
 | 
			
		||||
                    metricsLastHour={featureMetrics.lastHour[feature.name]}
 | 
			
		||||
                    metricsLastMinute={featureMetrics.lastMinute[feature.name]}
 | 
			
		||||
                    feature={feature}
 | 
			
		||||
                    toggleFeature={jest.fn()}
 | 
			
		||||
                    hasAccess={() => true}
 | 
			
		||||
                />
 | 
			
		||||
            </ThemeProvider>
 | 
			
		||||
@ -63,18 +55,12 @@ test('renders correctly with one feature without permission', () => {
 | 
			
		||||
        ],
 | 
			
		||||
        createdAt: '2018-02-04T20:27:52.127Z',
 | 
			
		||||
    };
 | 
			
		||||
    const featureMetrics = { lastHour: {}, lastMinute: {}, seenApps: {} };
 | 
			
		||||
    const settings = { sort: 'name' };
 | 
			
		||||
    const tree = renderer.create(
 | 
			
		||||
        <MemoryRouter>
 | 
			
		||||
            <ThemeProvider theme={theme}>
 | 
			
		||||
                <FeatureToggleListItem
 | 
			
		||||
                    key={0}
 | 
			
		||||
                    settings={settings}
 | 
			
		||||
                    metricsLastHour={featureMetrics.lastHour[feature.name]}
 | 
			
		||||
                    metricsLastMinute={featureMetrics.lastMinute[feature.name]}
 | 
			
		||||
                    feature={feature}
 | 
			
		||||
                    toggleFeature={jest.fn()}
 | 
			
		||||
                    hasAccess={() => true}
 | 
			
		||||
                />
 | 
			
		||||
            </ThemeProvider>
 | 
			
		||||
 | 
			
		||||
@ -25,8 +25,7 @@ test('renders correctly with one feature', () => {
 | 
			
		||||
            name: 'Another',
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
    const featureMetrics = { lastHour: {}, lastMinute: {}, seenApps: {} };
 | 
			
		||||
    const settings = { sort: 'name' };
 | 
			
		||||
 | 
			
		||||
    const tree = renderer.create(
 | 
			
		||||
        <MemoryRouter>
 | 
			
		||||
            <ThemeProvider theme={theme}>
 | 
			
		||||
@ -35,13 +34,12 @@ test('renders correctly with one feature', () => {
 | 
			
		||||
                >
 | 
			
		||||
                    <FeatureToggleList
 | 
			
		||||
                        updateSetting={jest.fn()}
 | 
			
		||||
                        settings={settings}
 | 
			
		||||
                        history={{}}
 | 
			
		||||
                        featureMetrics={featureMetrics}
 | 
			
		||||
                        filter={{}}
 | 
			
		||||
                        setFilter={jest.fn()}
 | 
			
		||||
                        sort={{}}
 | 
			
		||||
                        setSort={jest.fn()}
 | 
			
		||||
                        features={features}
 | 
			
		||||
                        toggleFeature={jest.fn()}
 | 
			
		||||
                        fetcher={jest.fn()}
 | 
			
		||||
                        currentProjectId="default"
 | 
			
		||||
                        flags={{}}
 | 
			
		||||
                    />
 | 
			
		||||
                </AccessProvider>
 | 
			
		||||
@ -58,8 +56,6 @@ test('renders correctly with one feature without permissions', () => {
 | 
			
		||||
            name: 'Another',
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
    const featureMetrics = { lastHour: {}, lastMinute: {}, seenApps: {} };
 | 
			
		||||
    const settings = { sort: 'name' };
 | 
			
		||||
    const tree = renderer.create(
 | 
			
		||||
        <MemoryRouter>
 | 
			
		||||
            <ThemeProvider theme={theme}>
 | 
			
		||||
@ -67,14 +63,12 @@ test('renders correctly with one feature without permissions', () => {
 | 
			
		||||
                    store={createFakeStore([{ permission: CREATE_FEATURE }])}
 | 
			
		||||
                >
 | 
			
		||||
                    <FeatureToggleList
 | 
			
		||||
                        updateSetting={jest.fn()}
 | 
			
		||||
                        settings={settings}
 | 
			
		||||
                        history={{}}
 | 
			
		||||
                        featureMetrics={featureMetrics}
 | 
			
		||||
                        filter={{}}
 | 
			
		||||
                        setFilter={jest.fn()}
 | 
			
		||||
                        sort={{}}
 | 
			
		||||
                        setSort={jest.fn()}
 | 
			
		||||
                        features={features}
 | 
			
		||||
                        toggleFeature={jest.fn()}
 | 
			
		||||
                        fetcher={jest.fn()}
 | 
			
		||||
                        currentProjectId="default"
 | 
			
		||||
                        flags={{}}
 | 
			
		||||
                    />
 | 
			
		||||
                </AccessProvider>
 | 
			
		||||
 | 
			
		||||
@ -1,151 +0,0 @@
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import {
 | 
			
		||||
    toggleFeature,
 | 
			
		||||
    fetchFeatureToggles,
 | 
			
		||||
} from '../../../store/feature-toggle/actions';
 | 
			
		||||
import { updateSettingForGroup } from '../../../store/settings/actions';
 | 
			
		||||
import FeatureToggleList from './FeatureToggleList';
 | 
			
		||||
 | 
			
		||||
function checkConstraints(strategy, regex) {
 | 
			
		||||
    if (!strategy.constraints) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    return strategy.constraints.some(c => c.values.some(v => regex.test(v)));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function resolveCurrentProjectId(settings) {
 | 
			
		||||
    if (!settings.currentProjectId || settings.currentProjectId === '*') {
 | 
			
		||||
        return 'default';
 | 
			
		||||
    }
 | 
			
		||||
    return settings.currentProjectId;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const mapStateToPropsConfigurable = isFeature => state => {
 | 
			
		||||
    const featureMetrics = state.featureMetrics.toJS();
 | 
			
		||||
    const flags = state.uiConfig.toJS().flags;
 | 
			
		||||
    const settings = state.settings.toJS().feature || {};
 | 
			
		||||
    let features = isFeature
 | 
			
		||||
        ? state.features.toJS()
 | 
			
		||||
        : state.archive.get('list').toArray();
 | 
			
		||||
 | 
			
		||||
    if (settings.currentProjectId && settings.currentProjectId !== '*') {
 | 
			
		||||
        features = features.filter(
 | 
			
		||||
            f => f.project === settings.currentProjectId
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    if (settings.filter) {
 | 
			
		||||
        try {
 | 
			
		||||
            const regex = new RegExp(settings.filter, 'i');
 | 
			
		||||
            features = features.filter(feature => {
 | 
			
		||||
                if (!isFeature) {
 | 
			
		||||
                    return (
 | 
			
		||||
                        regex.test(feature.name) ||
 | 
			
		||||
                        regex.test(feature.description) ||
 | 
			
		||||
                        (settings.filter.length > 1 &&
 | 
			
		||||
                            regex.test(JSON.stringify(feature)))
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
                return (
 | 
			
		||||
                    feature.strategies.some(s => checkConstraints(s, regex)) ||
 | 
			
		||||
                    regex.test(feature.name) ||
 | 
			
		||||
                    regex.test(feature.description) ||
 | 
			
		||||
                    feature.strategies.some(
 | 
			
		||||
                        s => s && s.name && regex.test(s.name)
 | 
			
		||||
                    ) ||
 | 
			
		||||
                    (settings.filter.length > 1 &&
 | 
			
		||||
                        regex.test(JSON.stringify(feature)))
 | 
			
		||||
                );
 | 
			
		||||
            });
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            // Invalid filter regex
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!settings.sort) {
 | 
			
		||||
        settings.sort = 'name';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (settings.sort === 'enabled') {
 | 
			
		||||
        features = features.sort((a, b) =>
 | 
			
		||||
            // eslint-disable-next-line
 | 
			
		||||
            a.enabled === b.enabled ? 0 : a.enabled ? -1 : 1
 | 
			
		||||
        );
 | 
			
		||||
    } else if (settings.sort === 'stale') {
 | 
			
		||||
        features = features.sort((a, b) =>
 | 
			
		||||
            // eslint-disable-next-line
 | 
			
		||||
            a.stale === b.stale ? 0 : a.stale ? -1 : 1
 | 
			
		||||
        );
 | 
			
		||||
    } else if (settings.sort === 'created') {
 | 
			
		||||
        features = features.sort((a, b) =>
 | 
			
		||||
            new Date(a.createdAt) > new Date(b.createdAt) ? -1 : 1
 | 
			
		||||
        );
 | 
			
		||||
    } else if (settings.sort === 'Last seen') {
 | 
			
		||||
        features = features.sort((a, b) =>
 | 
			
		||||
            new Date(a.lastSeenAt) > new Date(b.lastSeenAt) ? -1 : 1
 | 
			
		||||
        );
 | 
			
		||||
    } else if (settings.sort === 'name') {
 | 
			
		||||
        features = features.sort((a, b) => {
 | 
			
		||||
            if (a.name < b.name) {
 | 
			
		||||
                return -1;
 | 
			
		||||
            }
 | 
			
		||||
            if (a.name > b.name) {
 | 
			
		||||
                return 1;
 | 
			
		||||
            }
 | 
			
		||||
            return 0;
 | 
			
		||||
        });
 | 
			
		||||
    } else if (settings.sort === 'project') {
 | 
			
		||||
        features = features.sort((a, b) =>
 | 
			
		||||
            a.project.length > b.project.length ? -1 : 1
 | 
			
		||||
        );
 | 
			
		||||
    } else if (settings.sort === 'type') {
 | 
			
		||||
        features = features.sort((a, b) => {
 | 
			
		||||
            if (a.type < b.type) {
 | 
			
		||||
                return -1;
 | 
			
		||||
            }
 | 
			
		||||
            if (a.type > b.type) {
 | 
			
		||||
                return 1;
 | 
			
		||||
            }
 | 
			
		||||
            return 0;
 | 
			
		||||
        });
 | 
			
		||||
    } else if (settings.sort === 'metrics') {
 | 
			
		||||
        const target = settings.showLastHour
 | 
			
		||||
            ? featureMetrics.lastHour
 | 
			
		||||
            : featureMetrics.lastMinute;
 | 
			
		||||
 | 
			
		||||
        features = features.sort((a, b) => {
 | 
			
		||||
            if (!target[a.name]) {
 | 
			
		||||
                return 1;
 | 
			
		||||
            }
 | 
			
		||||
            if (!target[b.name]) {
 | 
			
		||||
                return -1;
 | 
			
		||||
            }
 | 
			
		||||
            if (target[a.name].yes > target[b.name].yes) {
 | 
			
		||||
                return -1;
 | 
			
		||||
            }
 | 
			
		||||
            return 1;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        features,
 | 
			
		||||
        currentProjectId: resolveCurrentProjectId(settings),
 | 
			
		||||
        featureMetrics,
 | 
			
		||||
        archive: !isFeature,
 | 
			
		||||
        settings,
 | 
			
		||||
        flags,
 | 
			
		||||
        loading: state.apiCalls.fetchTogglesState.loading,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
const mapStateToProps = mapStateToPropsConfigurable(true);
 | 
			
		||||
const mapDispatchToProps = {
 | 
			
		||||
    toggleFeature,
 | 
			
		||||
    fetcher: () => fetchFeatureToggles(),
 | 
			
		||||
    updateSetting: updateSettingForGroup('feature'),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const FeatureToggleListContainer = connect(
 | 
			
		||||
    mapStateToProps,
 | 
			
		||||
    mapDispatchToProps
 | 
			
		||||
)(FeatureToggleList);
 | 
			
		||||
 | 
			
		||||
export default FeatureToggleListContainer;
 | 
			
		||||
@ -1,25 +1,21 @@
 | 
			
		||||
import { Tooltip } from '@material-ui/core';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import { formatDateWithLocale, formatFullDateTimeWithLocale } from '../../../common/util';
 | 
			
		||||
import { useLocationSettings } from "../../../../hooks/useLocationSettings";
 | 
			
		||||
 | 
			
		||||
interface CreatedAtProps {
 | 
			
		||||
    time: Date;
 | 
			
		||||
    //@ts-ignore
 | 
			
		||||
    location: any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CreatedAt = ({time, location}: CreatedAtProps) => {
 | 
			
		||||
const CreatedAt = ({time}: CreatedAtProps) => {
 | 
			
		||||
    const { locationSettings } = useLocationSettings();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Tooltip title={`Created at ${formatFullDateTimeWithLocale(time, location.locale)}`}>
 | 
			
		||||
        <Tooltip title={`Created at ${formatFullDateTimeWithLocale(time, locationSettings.locale)}`}>
 | 
			
		||||
            <span>
 | 
			
		||||
                {formatDateWithLocale(time, location.locale)}
 | 
			
		||||
                {formatDateWithLocale(time, locationSettings.locale)}
 | 
			
		||||
            </span>
 | 
			
		||||
        </Tooltip>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = (state: any) => ({
 | 
			
		||||
    location: state.settings.toJS().location,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps)(CreatedAt);
 | 
			
		||||
export default CreatedAt;
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import { Redirect, useParams } from 'react-router-dom';
 | 
			
		||||
import useFeatures from '../../../hooks/api/getters/useFeatures/useFeatures';
 | 
			
		||||
import { useFeatures } from '../../../hooks/api/getters/useFeatures/useFeatures';
 | 
			
		||||
import { IFeatureToggle } from '../../../interfaces/featureToggle';
 | 
			
		||||
import { getTogglePath } from '../../../utils/route-path-helpers';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -50,9 +50,8 @@ const useFeatureForm = (
 | 
			
		||||
        return {
 | 
			
		||||
            type,
 | 
			
		||||
            name,
 | 
			
		||||
            projectId: project,
 | 
			
		||||
            description: description,
 | 
			
		||||
            impressionData
 | 
			
		||||
            description,
 | 
			
		||||
            impressionData,
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -11,24 +11,21 @@ import EventCard from './EventCard/EventCard';
 | 
			
		||||
import { useStyles } from './EventLog.styles.js';
 | 
			
		||||
 | 
			
		||||
const EventLog = ({
 | 
			
		||||
    updateSetting,
 | 
			
		||||
    title,
 | 
			
		||||
    history,
 | 
			
		||||
    settings,
 | 
			
		||||
    eventSettings,
 | 
			
		||||
    setEventSettings,
 | 
			
		||||
    locationSettings,
 | 
			
		||||
    displayInline,
 | 
			
		||||
    location,
 | 
			
		||||
    hideName,
 | 
			
		||||
}) => {
 | 
			
		||||
    const styles = useStyles();
 | 
			
		||||
    const toggleShowDiff = () => {
 | 
			
		||||
        updateSetting('showData', !settings.showData);
 | 
			
		||||
        setEventSettings({ showData: !eventSettings.showData });
 | 
			
		||||
    };
 | 
			
		||||
    const formatFulldateTime = v => {
 | 
			
		||||
        return formatFullDateTimeWithLocale(v, location.locale);
 | 
			
		||||
        return formatFullDateTimeWithLocale(v, locationSettings.locale);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const showData = settings.showData;
 | 
			
		||||
 | 
			
		||||
    if (!history || history.length < 0) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
@ -44,7 +41,7 @@ const EventLog = ({
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (showData) {
 | 
			
		||||
    if (eventSettings.showData) {
 | 
			
		||||
        entries = history.map(entry => (
 | 
			
		||||
            <EventJson key={`log${entry.id}`} entry={entry} />
 | 
			
		||||
        ));
 | 
			
		||||
@ -63,7 +60,7 @@ const EventLog = ({
 | 
			
		||||
                        <FormControlLabel
 | 
			
		||||
                            control={
 | 
			
		||||
                                <Switch
 | 
			
		||||
                                    checked={showData}
 | 
			
		||||
                                    checked={eventSettings.showData}
 | 
			
		||||
                                    onChange={toggleShowDiff}
 | 
			
		||||
                                    color="primary"
 | 
			
		||||
                                />
 | 
			
		||||
@ -82,12 +79,12 @@ const EventLog = ({
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
EventLog.propTypes = {
 | 
			
		||||
    updateSettings: PropTypes.func,
 | 
			
		||||
    history: PropTypes.array,
 | 
			
		||||
    eventSettings: PropTypes.object.isRequired,
 | 
			
		||||
    setEventSettings: PropTypes.func.isRequired,
 | 
			
		||||
    locationSettings: PropTypes.object.isRequired,
 | 
			
		||||
    title: PropTypes.string,
 | 
			
		||||
    settings: PropTypes.object,
 | 
			
		||||
    displayInline: PropTypes.bool,
 | 
			
		||||
    location: PropTypes.object,
 | 
			
		||||
    hideName: PropTypes.bool,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default EventLog;
 | 
			
		||||
 | 
			
		||||
@ -1,18 +0,0 @@
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import EventLog from './EventLog';
 | 
			
		||||
import { updateSettingForGroup } from '../../../store/settings/actions';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => {
 | 
			
		||||
    const settings = state.settings.toJS().history || {};
 | 
			
		||||
    const location = state.settings.toJS().location || {};
 | 
			
		||||
    return {
 | 
			
		||||
        settings,
 | 
			
		||||
        location,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const EventLogContainer = connect(mapStateToProps, {
 | 
			
		||||
    updateSetting: updateSettingForGroup('history'),
 | 
			
		||||
})(EventLog);
 | 
			
		||||
 | 
			
		||||
export default EventLogContainer;
 | 
			
		||||
							
								
								
									
										27
									
								
								frontend/src/component/history/EventLog/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								frontend/src/component/history/EventLog/index.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
import EventLog from './EventLog';
 | 
			
		||||
import { useEventSettings } from "../../../hooks/useEventSettings";
 | 
			
		||||
import { useLocationSettings } from "../../../hooks/useLocationSettings";
 | 
			
		||||
 | 
			
		||||
interface IEventLogContainerProps {
 | 
			
		||||
    title: string;
 | 
			
		||||
    history: unknown[];
 | 
			
		||||
    displayInline?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const EventLogContainer = (props: IEventLogContainerProps) => {
 | 
			
		||||
    const { locationSettings } = useLocationSettings();
 | 
			
		||||
    const { eventSettings, setEventSettings } = useEventSettings();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <EventLog
 | 
			
		||||
            title={props.title}
 | 
			
		||||
            history={props.history}
 | 
			
		||||
            eventSettings={eventSettings}
 | 
			
		||||
            setEventSettings={setEventSettings}
 | 
			
		||||
            locationSettings={locationSettings}
 | 
			
		||||
            displayInline={props.displayInline}
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default EventLogContainer;
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,13 @@
 | 
			
		||||
import CopyFeatureToggle from '../../page/features/copy';
 | 
			
		||||
import Features from '../../page/features';
 | 
			
		||||
import { FeatureToggleListContainer } from '../feature/FeatureToggleList/FeatureToggleListContainer';
 | 
			
		||||
import CreateStrategies from '../../page/strategies/create';
 | 
			
		||||
import StrategyView from '../../page/strategies/show';
 | 
			
		||||
import Strategies from '../../page/strategies';
 | 
			
		||||
import HistoryPage from '../../page/history';
 | 
			
		||||
import HistoryTogglePage from '../../page/history/toggle';
 | 
			
		||||
import Archive from '../../page/archive';
 | 
			
		||||
import { ArchiveListContainer } from '../archive/ArchiveListContainer';
 | 
			
		||||
import Applications from '../../page/applications';
 | 
			
		||||
import ApplicationView from '../../page/applications/view';
 | 
			
		||||
import ListTagTypes from '../../page/tag-types';
 | 
			
		||||
import Addons from '../../page/addons';
 | 
			
		||||
import AddonsCreate from '../../page/addons/create';
 | 
			
		||||
@ -14,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';
 | 
			
		||||
@ -22,7 +24,7 @@ import ResetPassword from '../user/ResetPassword/ResetPassword';
 | 
			
		||||
import ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
 | 
			
		||||
import ProjectListNew from '../project/ProjectList/ProjectList';
 | 
			
		||||
import Project from '../project/Project/Project';
 | 
			
		||||
import RedirectArchive from '../feature/RedirectArchive/RedirectArchive';
 | 
			
		||||
import RedirectArchive from '../archive/RedirectArchive';
 | 
			
		||||
import EnvironmentList from '../environments/EnvironmentList/EnvironmentList';
 | 
			
		||||
import FeatureView from '../feature/FeatureView/FeatureView';
 | 
			
		||||
import ProjectRoles from '../admin/project-roles/ProjectRoles/ProjectRoles';
 | 
			
		||||
@ -183,7 +185,7 @@ export const routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '/features',
 | 
			
		||||
        title: 'Feature Toggles',
 | 
			
		||||
        component: Features,
 | 
			
		||||
        component: FeatureToggleListContainer,
 | 
			
		||||
        type: 'protected',
 | 
			
		||||
        layout: 'main',
 | 
			
		||||
        menu: { mobile: true },
 | 
			
		||||
@ -373,7 +375,7 @@ export const routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '/archive',
 | 
			
		||||
        title: 'Archived Toggles',
 | 
			
		||||
        component: Archive,
 | 
			
		||||
        component: ArchiveListContainer,
 | 
			
		||||
        type: 'protected',
 | 
			
		||||
        layout: 'main',
 | 
			
		||||
        menu: {},
 | 
			
		||||
@ -447,7 +449,7 @@ export const routes = [
 | 
			
		||||
        path: '/admin/auth',
 | 
			
		||||
        parent: '/admin',
 | 
			
		||||
        title: 'Single Sign-On',
 | 
			
		||||
        component: AdminAuth,
 | 
			
		||||
        component: AuthSettings,
 | 
			
		||||
        type: 'protected',
 | 
			
		||||
        layout: 'main',
 | 
			
		||||
        menu: { adminSettings: true },
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,5 @@
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import classnames from 'classnames';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import OutsideClickHandler from 'react-outside-click-handler';
 | 
			
		||||
 | 
			
		||||
import { Avatar, Button } from '@material-ui/core';
 | 
			
		||||
@ -9,16 +8,18 @@ import { useStyles } from './UserProfile.styles';
 | 
			
		||||
import { useCommonStyles } from '../../../common.styles';
 | 
			
		||||
import UserProfileContent from './UserProfileContent/UserProfileContent';
 | 
			
		||||
import { IUser } from "../../../interfaces/user";
 | 
			
		||||
import { ILocationSettings } from "../../../hooks/useLocationSettings";
 | 
			
		||||
 | 
			
		||||
interface IUserProfileProps {
 | 
			
		||||
    profile: IUser
 | 
			
		||||
    updateSettingLocation: (field: 'locale', value: string) => void
 | 
			
		||||
    locationSettings: ILocationSettings
 | 
			
		||||
    setLocationSettings: React.Dispatch<React.SetStateAction<ILocationSettings>>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const UserProfile = ({
 | 
			
		||||
    profile,
 | 
			
		||||
    location,
 | 
			
		||||
    updateSettingLocation,
 | 
			
		||||
    locationSettings,
 | 
			
		||||
    setLocationSettings,
 | 
			
		||||
}: IUserProfileProps) => {
 | 
			
		||||
    const [showProfile, setShowProfile] = useState(false);
 | 
			
		||||
    const [currentLocale, setCurrentLocale] = useState<string>();
 | 
			
		||||
@ -40,17 +41,15 @@ const UserProfile = ({
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        const locale = location.locale || navigator.language;
 | 
			
		||||
        let found = possibleLocales.find(l =>
 | 
			
		||||
            l.toLowerCase().includes(locale.toLowerCase())
 | 
			
		||||
            l.toLowerCase().includes(locationSettings.locale.toLowerCase())
 | 
			
		||||
        );
 | 
			
		||||
        setCurrentLocale(found);
 | 
			
		||||
 | 
			
		||||
        if (!found) {
 | 
			
		||||
            setPossibleLocales(prev => [...prev, locale]);
 | 
			
		||||
            setPossibleLocales(prev => [...prev, locationSettings.locale]);
 | 
			
		||||
        }
 | 
			
		||||
        /* eslint-disable-next-line*/
 | 
			
		||||
    }, []);
 | 
			
		||||
    }, [locationSettings]);
 | 
			
		||||
 | 
			
		||||
    const email = profile ? profile.email : '';
 | 
			
		||||
    const imageUrl = email ? profile.imageUrl : 'unknown-user.png';
 | 
			
		||||
@ -75,7 +74,7 @@ const UserProfile = ({
 | 
			
		||||
                    showProfile={showProfile}
 | 
			
		||||
                    imageUrl={imageUrl}
 | 
			
		||||
                    profile={profile}
 | 
			
		||||
                    updateSettingLocation={updateSettingLocation}
 | 
			
		||||
                    setLocationSettings={setLocationSettings}
 | 
			
		||||
                    possibleLocales={possibleLocales}
 | 
			
		||||
                    setCurrentLocale={setCurrentLocale}
 | 
			
		||||
                    currentLocale={currentLocale}
 | 
			
		||||
@ -85,10 +84,4 @@ const UserProfile = ({
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
UserProfile.propTypes = {
 | 
			
		||||
    profile: PropTypes.object,
 | 
			
		||||
    location: PropTypes.object,
 | 
			
		||||
    updateSettingLocation: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default UserProfile;
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,13 @@
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import ConditionallyRender from '../../../common/ConditionallyRender';
 | 
			
		||||
import {
 | 
			
		||||
    Paper,
 | 
			
		||||
    Avatar,
 | 
			
		||||
    Typography,
 | 
			
		||||
    Button,
 | 
			
		||||
    FormControl,
 | 
			
		||||
    Select,
 | 
			
		||||
    InputLabel,
 | 
			
		||||
    Paper,
 | 
			
		||||
    Select,
 | 
			
		||||
    Typography,
 | 
			
		||||
} from '@material-ui/core';
 | 
			
		||||
import classnames from 'classnames';
 | 
			
		||||
import { useStyles } from './UserProfileContent.styles';
 | 
			
		||||
@ -17,26 +17,29 @@ import EditProfile from '../EditProfile/EditProfile';
 | 
			
		||||
import legacyStyles from '../../user.module.scss';
 | 
			
		||||
import { getBasePath } from '../../../../utils/format-path';
 | 
			
		||||
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import { IUser } from "../../../../interfaces/user";
 | 
			
		||||
import { IUser } from '../../../../interfaces/user';
 | 
			
		||||
import { ILocationSettings } from '../../../../hooks/useLocationSettings';
 | 
			
		||||
 | 
			
		||||
interface IUserProfileContentProps {
 | 
			
		||||
    showProfile: boolean
 | 
			
		||||
    profile: IUser
 | 
			
		||||
    possibleLocales: string[]
 | 
			
		||||
    updateSettingLocation: (field: 'locale', value: string) => void
 | 
			
		||||
    imageUrl: string
 | 
			
		||||
    currentLocale?: string
 | 
			
		||||
    setCurrentLocale: (value: string) => void
 | 
			
		||||
    showProfile: boolean;
 | 
			
		||||
    profile: IUser;
 | 
			
		||||
    possibleLocales: string[];
 | 
			
		||||
    imageUrl: string;
 | 
			
		||||
    currentLocale?: string;
 | 
			
		||||
    setCurrentLocale: (value: string) => void;
 | 
			
		||||
    setLocationSettings: React.Dispatch<
 | 
			
		||||
        React.SetStateAction<ILocationSettings>
 | 
			
		||||
    >;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const UserProfileContent = ({
 | 
			
		||||
    showProfile,
 | 
			
		||||
    profile,
 | 
			
		||||
    possibleLocales,
 | 
			
		||||
    updateSettingLocation,
 | 
			
		||||
    imageUrl,
 | 
			
		||||
    currentLocale,
 | 
			
		||||
    setCurrentLocale,
 | 
			
		||||
    setLocationSettings,
 | 
			
		||||
}: IUserProfileContentProps) => {
 | 
			
		||||
    const commonStyles = useCommonStyles();
 | 
			
		||||
    const { uiConfig } = useUiConfig();
 | 
			
		||||
@ -44,10 +47,6 @@ const UserProfileContent = ({
 | 
			
		||||
    const [editingProfile, setEditingProfile] = useState(false);
 | 
			
		||||
    const styles = useStyles();
 | 
			
		||||
 | 
			
		||||
    const setLocale = (value: string) => {
 | 
			
		||||
        updateSettingLocation('locale', value);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // @ts-expect-error
 | 
			
		||||
    const profileAvatarClasses = classnames(styles.avatar, {
 | 
			
		||||
        // @ts-expect-error
 | 
			
		||||
@ -61,9 +60,9 @@ const UserProfileContent = ({
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const handleChange = (e: React.ChangeEvent<{ value: unknown }>) => {
 | 
			
		||||
        const value = e.target.value as string;
 | 
			
		||||
        setCurrentLocale(value);
 | 
			
		||||
        setLocale(value);
 | 
			
		||||
        const locale = e.target.value as string;
 | 
			
		||||
        setCurrentLocale(locale);
 | 
			
		||||
        setLocationSettings({ locale });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
 | 
			
		||||
@ -1,27 +0,0 @@
 | 
			
		||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import UserProfile from './UserProfile';
 | 
			
		||||
import { updateSettingForGroup } from '../../../store/settings/actions';
 | 
			
		||||
 | 
			
		||||
const mapDispatchToProps = {
 | 
			
		||||
    updateSettingLocation: updateSettingForGroup('location'),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => ({
 | 
			
		||||
    location: state.settings ? state.settings.toJS().location : {},
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default connect(
 | 
			
		||||
    mapStateToProps,
 | 
			
		||||
    mapDispatchToProps
 | 
			
		||||
)(props => {
 | 
			
		||||
    const user = useUser();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <UserProfile
 | 
			
		||||
            location={props.location}
 | 
			
		||||
            updateSettingLocation={props.updateSettingLocation}
 | 
			
		||||
            profile={user.user}
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										18
									
								
								frontend/src/component/user/UserProfile/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								frontend/src/component/user/UserProfile/index.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
 | 
			
		||||
import UserProfile from './UserProfile';
 | 
			
		||||
import { useLocationSettings } from '../../../hooks/useLocationSettings';
 | 
			
		||||
 | 
			
		||||
const UserProfileContainer = () => {
 | 
			
		||||
    const user = useUser();
 | 
			
		||||
    const { locationSettings, setLocationSettings } = useLocationSettings();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <UserProfile
 | 
			
		||||
            locationSettings={locationSettings}
 | 
			
		||||
            setLocationSettings={setLocationSettings}
 | 
			
		||||
            profile={user.user}
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default UserProfileContainer;
 | 
			
		||||
@ -46,7 +46,7 @@ const useAPI = ({
 | 
			
		||||
    handleUnauthorized,
 | 
			
		||||
    propagateErrors = false,
 | 
			
		||||
}: IUseAPI) => {
 | 
			
		||||
    const [errors, setErrors] = useState({});
 | 
			
		||||
    const [errors, setErrors] = useState<Record<string, string>>({});
 | 
			
		||||
    const [loading, setLoading] = useState(false);
 | 
			
		||||
 | 
			
		||||
    const defaultOptions: RequestInit = {
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,15 @@
 | 
			
		||||
import useAPI from '../useApi/useApi';
 | 
			
		||||
 | 
			
		||||
export const useFeatureArchiveApi = () => {
 | 
			
		||||
    const { makeRequest, createRequest, errors, loading } = useAPI({
 | 
			
		||||
        propagateErrors: true,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const reviveFeature = async (feature: string) => {
 | 
			
		||||
        const path = `api/admin/archive/revive/${feature}`;
 | 
			
		||||
        const req = createRequest(path, { method: 'POST' });
 | 
			
		||||
        return makeRequest(req.caller, req.id);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return { reviveFeature, errors, loading };
 | 
			
		||||
};
 | 
			
		||||
@ -1,40 +1,39 @@
 | 
			
		||||
import useSWR, { mutate, SWRConfiguration } from 'swr';
 | 
			
		||||
import { useState, useEffect } from 'react';
 | 
			
		||||
import { useCallback } from 'react';
 | 
			
		||||
import { formatApiPath } from '../../../../utils/format-path';
 | 
			
		||||
import handleErrorResponses from '../httpErrorResponseHandler';
 | 
			
		||||
import { IFeatureToggle } from '../../../../interfaces/featureToggle';
 | 
			
		||||
 | 
			
		||||
const useFeatures = (options: SWRConfiguration = {}) => {
 | 
			
		||||
    const fetcher = async () => {
 | 
			
		||||
        const path = formatApiPath('api/admin/features/');
 | 
			
		||||
        return fetch(path, {
 | 
			
		||||
            method: 'GET',
 | 
			
		||||
        })
 | 
			
		||||
            .then(handleErrorResponses('Features'))
 | 
			
		||||
            .then(res => res.json());
 | 
			
		||||
    };
 | 
			
		||||
const PATH = formatApiPath('api/admin/features');
 | 
			
		||||
 | 
			
		||||
    const FEATURES_CACHE_KEY = 'api/admin/features/';
 | 
			
		||||
export interface IUseFeaturesOutput {
 | 
			
		||||
    features: IFeatureToggle[];
 | 
			
		||||
    refetchFeatures: () => void;
 | 
			
		||||
    loading: boolean;
 | 
			
		||||
    error?: Error;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    const { data, error } = useSWR(FEATURES_CACHE_KEY, fetcher, {
 | 
			
		||||
        ...options,
 | 
			
		||||
    });
 | 
			
		||||
export const useFeatures = (options?: SWRConfiguration): IUseFeaturesOutput => {
 | 
			
		||||
    const { data, error } = useSWR<{ features: IFeatureToggle[] }>(
 | 
			
		||||
        PATH,
 | 
			
		||||
        fetchFeatures,
 | 
			
		||||
        options
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const [loading, setLoading] = useState(!error && !data);
 | 
			
		||||
 | 
			
		||||
    const refetchFeatures = () => {
 | 
			
		||||
        mutate(FEATURES_CACHE_KEY);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setLoading(!error && !data);
 | 
			
		||||
    }, [data, error]);
 | 
			
		||||
    const refetchFeatures = useCallback(() => {
 | 
			
		||||
        mutate(PATH).catch(console.warn);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        features: data?.features || [],
 | 
			
		||||
        error,
 | 
			
		||||
        loading,
 | 
			
		||||
        loading: !error && !data,
 | 
			
		||||
        refetchFeatures,
 | 
			
		||||
        error,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default useFeatures;
 | 
			
		||||
const fetchFeatures = () => {
 | 
			
		||||
    return fetch(PATH, { method: 'GET' })
 | 
			
		||||
        .then(handleErrorResponses('Features'))
 | 
			
		||||
        .then(res => res.json());
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,41 @@
 | 
			
		||||
import useSWR, { mutate, SWRConfiguration } from 'swr';
 | 
			
		||||
import { useCallback } from 'react';
 | 
			
		||||
import { formatApiPath } from '../../../../utils/format-path';
 | 
			
		||||
import handleErrorResponses from '../httpErrorResponseHandler';
 | 
			
		||||
import { IFeatureToggle } from '../../../../interfaces/featureToggle';
 | 
			
		||||
 | 
			
		||||
const PATH = formatApiPath('api/admin/archive/features');
 | 
			
		||||
 | 
			
		||||
export interface UseFeaturesArchiveOutput {
 | 
			
		||||
    archivedFeatures: IFeatureToggle[];
 | 
			
		||||
    refetchArchived: () => void;
 | 
			
		||||
    loading: boolean;
 | 
			
		||||
    error?: Error;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useFeaturesArchive = (
 | 
			
		||||
    options?: SWRConfiguration
 | 
			
		||||
): UseFeaturesArchiveOutput => {
 | 
			
		||||
    const { data, error } = useSWR<{ features: IFeatureToggle[] }>(
 | 
			
		||||
        PATH,
 | 
			
		||||
        fetchArchivedFeatures,
 | 
			
		||||
        options
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const refetchArchived = useCallback(() => {
 | 
			
		||||
        mutate(PATH).catch(console.warn);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        archivedFeatures: data?.features || [],
 | 
			
		||||
        refetchArchived,
 | 
			
		||||
        loading: !error && !data,
 | 
			
		||||
        error,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const fetchArchivedFeatures = () => {
 | 
			
		||||
    return fetch(PATH, { method: 'GET' })
 | 
			
		||||
        .then(handleErrorResponses('Archive'))
 | 
			
		||||
        .then(res => res.json());
 | 
			
		||||
};
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								frontend/src/hooks/useEventSettings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								frontend/src/hooks/useEventSettings.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
import { getBasePath } from '../utils/format-path';
 | 
			
		||||
import { createPersistentGlobalState } from './usePersistentGlobalState';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
export interface IEventSettings {
 | 
			
		||||
    showData: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IUseEventSettingsOutput {
 | 
			
		||||
    eventSettings: IEventSettings;
 | 
			
		||||
    setEventSettings: React.Dispatch<React.SetStateAction<IEventSettings>>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useEventSettings = (): IUseEventSettingsOutput => {
 | 
			
		||||
    const [eventSettings, setEventSettings] = useGlobalState();
 | 
			
		||||
 | 
			
		||||
    return { eventSettings, setEventSettings };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createInitialValue = (): IEventSettings => {
 | 
			
		||||
    return { showData: false };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const useGlobalState = createPersistentGlobalState<IEventSettings>(
 | 
			
		||||
    `${getBasePath()}:useEventSettings:v1`,
 | 
			
		||||
    createInitialValue()
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										117
									
								
								frontend/src/hooks/useFeaturesFilter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								frontend/src/hooks/useFeaturesFilter.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,117 @@
 | 
			
		||||
import { IFeatureToggle } from '../interfaces/featureToggle';
 | 
			
		||||
import React, { useMemo } from 'react';
 | 
			
		||||
import { getBasePath } from '../utils/format-path';
 | 
			
		||||
import { createPersistentGlobalState } from './usePersistentGlobalState';
 | 
			
		||||
 | 
			
		||||
export interface IFeaturesFilter {
 | 
			
		||||
    query?: string;
 | 
			
		||||
    project: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IFeaturesSortOutput {
 | 
			
		||||
    filtered: IFeatureToggle[];
 | 
			
		||||
    filter: IFeaturesFilter;
 | 
			
		||||
    setFilter: React.Dispatch<React.SetStateAction<IFeaturesFilter>>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Store the features filter state globally, and in localStorage.
 | 
			
		||||
// When changing the format of IFeaturesFilter, change the version as well.
 | 
			
		||||
const useFeaturesFilterState = createPersistentGlobalState<IFeaturesFilter>(
 | 
			
		||||
    `${getBasePath()}:useFeaturesFilter:v1`,
 | 
			
		||||
    { project: '*' }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const useFeaturesFilter = (
 | 
			
		||||
    features: IFeatureToggle[]
 | 
			
		||||
): IFeaturesSortOutput => {
 | 
			
		||||
    const [filter, setFilter] = useFeaturesFilterState();
 | 
			
		||||
 | 
			
		||||
    const filtered = useMemo(() => {
 | 
			
		||||
        return filterFeatures(features, filter);
 | 
			
		||||
    }, [features, filter]);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        setFilter,
 | 
			
		||||
        filter,
 | 
			
		||||
        filtered,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Return the current project ID a project has been selected,
 | 
			
		||||
// or the 'default' project if showing all projects.
 | 
			
		||||
export const resolveFilteredProjectId = (filter: IFeaturesFilter): string => {
 | 
			
		||||
    if (!filter.project || filter.project === '*') {
 | 
			
		||||
        return 'default';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return filter.project;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const filterFeatures = (
 | 
			
		||||
    features: IFeatureToggle[],
 | 
			
		||||
    filter: IFeaturesFilter
 | 
			
		||||
): IFeatureToggle[] => {
 | 
			
		||||
    return filterFeaturesByQuery(
 | 
			
		||||
        filterFeaturesByProject(features, filter),
 | 
			
		||||
        filter
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const filterFeaturesByProject = (
 | 
			
		||||
    features: IFeatureToggle[],
 | 
			
		||||
    filter: IFeaturesFilter
 | 
			
		||||
): IFeatureToggle[] => {
 | 
			
		||||
    return filter.project === '*'
 | 
			
		||||
        ? features
 | 
			
		||||
        : features.filter(f => f.project === filter.project);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const filterFeaturesByQuery = (
 | 
			
		||||
    features: IFeatureToggle[],
 | 
			
		||||
    filter: IFeaturesFilter
 | 
			
		||||
): IFeatureToggle[] => {
 | 
			
		||||
    if (!filter.query) {
 | 
			
		||||
        return features;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Try to parse the search query as a RegExp.
 | 
			
		||||
    // Return all features if it can't be parsed.
 | 
			
		||||
    try {
 | 
			
		||||
        const regExp = new RegExp(filter.query, 'i');
 | 
			
		||||
        return features.filter(f => filterFeatureByRegExp(f, filter, regExp));
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
        if (err instanceof SyntaxError) {
 | 
			
		||||
            return features;
 | 
			
		||||
        } else {
 | 
			
		||||
            throw err;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const filterFeatureByRegExp = (
 | 
			
		||||
    feature: IFeatureToggle,
 | 
			
		||||
    filter: IFeaturesFilter,
 | 
			
		||||
    regExp: RegExp
 | 
			
		||||
): boolean => {
 | 
			
		||||
    if (regExp.test(feature.name) || regExp.test(feature.description)) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        filter.query &&
 | 
			
		||||
        filter.query.length > 1 &&
 | 
			
		||||
        regExp.test(JSON.stringify(feature))
 | 
			
		||||
    ) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!feature.strategies) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return feature.strategies.some(
 | 
			
		||||
        s =>
 | 
			
		||||
            regExp.test(s.name) ||
 | 
			
		||||
            s.constraints.some(c => c.values.some(v => regExp.test(v)))
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										137
									
								
								frontend/src/hooks/useFeaturesSort.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								frontend/src/hooks/useFeaturesSort.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,137 @@
 | 
			
		||||
import { IFeatureToggle } from '../interfaces/featureToggle';
 | 
			
		||||
import React, { useMemo } from 'react';
 | 
			
		||||
import { getBasePath } from '../utils/format-path';
 | 
			
		||||
import { createPersistentGlobalState } from './usePersistentGlobalState';
 | 
			
		||||
 | 
			
		||||
type FeaturesSortType =
 | 
			
		||||
    | 'name'
 | 
			
		||||
    | 'type'
 | 
			
		||||
    | 'enabled'
 | 
			
		||||
    | 'stale'
 | 
			
		||||
    | 'created'
 | 
			
		||||
    | 'last-seen'
 | 
			
		||||
    | 'project';
 | 
			
		||||
 | 
			
		||||
interface IFeaturesSort {
 | 
			
		||||
    type: FeaturesSortType;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IFeaturesSortOutput {
 | 
			
		||||
    sort: IFeaturesSort;
 | 
			
		||||
    sorted: IFeatureToggle[];
 | 
			
		||||
    setSort: React.Dispatch<React.SetStateAction<IFeaturesSort>>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IFeaturesFilterSortOption {
 | 
			
		||||
    type: FeaturesSortType;
 | 
			
		||||
    name: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Store the features sort state globally, and in localStorage.
 | 
			
		||||
// When changing the format of IFeaturesSort, change the version as well.
 | 
			
		||||
const useFeaturesSortState = createPersistentGlobalState<IFeaturesSort>(
 | 
			
		||||
    `${getBasePath()}:useFeaturesSort:v1`,
 | 
			
		||||
    { type: 'name' }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const useFeaturesSort = (
 | 
			
		||||
    features: IFeatureToggle[]
 | 
			
		||||
): IFeaturesSortOutput => {
 | 
			
		||||
    const [sort, setSort] = useFeaturesSortState();
 | 
			
		||||
 | 
			
		||||
    const sorted = useMemo(() => {
 | 
			
		||||
        return sortFeatures(features, sort);
 | 
			
		||||
    }, [features, sort]);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        setSort,
 | 
			
		||||
        sort,
 | 
			
		||||
        sorted,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const createFeaturesFilterSortOptions =
 | 
			
		||||
    (): IFeaturesFilterSortOption[] => {
 | 
			
		||||
        return [
 | 
			
		||||
            { type: 'name', name: 'Name' },
 | 
			
		||||
            { type: 'type', name: 'Type' },
 | 
			
		||||
            { type: 'enabled', name: 'Enabled' },
 | 
			
		||||
            { type: 'stale', name: 'Stale' },
 | 
			
		||||
            { type: 'created', name: 'Created' },
 | 
			
		||||
            { type: 'last-seen', name: 'Last seen' },
 | 
			
		||||
            { type: 'project', name: 'Project' },
 | 
			
		||||
        ];
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
const sortFeatures = (
 | 
			
		||||
    features: IFeatureToggle[],
 | 
			
		||||
    sort: IFeaturesSort
 | 
			
		||||
): IFeatureToggle[] => {
 | 
			
		||||
    switch (sort.type) {
 | 
			
		||||
        case 'enabled':
 | 
			
		||||
            return sortByEnabled(features);
 | 
			
		||||
        case 'stale':
 | 
			
		||||
            return sortByStale(features);
 | 
			
		||||
        case 'created':
 | 
			
		||||
            return sortByCreated(features);
 | 
			
		||||
        case 'last-seen':
 | 
			
		||||
            return sortByLastSeen(features);
 | 
			
		||||
        case 'name':
 | 
			
		||||
            return sortByName(features);
 | 
			
		||||
        case 'project':
 | 
			
		||||
            return sortByProject(features);
 | 
			
		||||
        case 'type':
 | 
			
		||||
            return sortByType(features);
 | 
			
		||||
        default:
 | 
			
		||||
            console.error(`Unknown feature sort type: ${sort.type}`);
 | 
			
		||||
            return features;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const sortByEnabled = (
 | 
			
		||||
    features: Readonly<IFeatureToggle[]>
 | 
			
		||||
): IFeatureToggle[] => {
 | 
			
		||||
    return [...features].sort((a, b) =>
 | 
			
		||||
        a.enabled === b.enabled ? 0 : a.enabled ? -1 : 1
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const sortByStale = (
 | 
			
		||||
    features: Readonly<IFeatureToggle[]>
 | 
			
		||||
): IFeatureToggle[] => {
 | 
			
		||||
    return [...features].sort((a, b) =>
 | 
			
		||||
        a.stale === b.stale ? 0 : a.stale ? -1 : 1
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const sortByLastSeen = (
 | 
			
		||||
    features: Readonly<IFeatureToggle[]>
 | 
			
		||||
): IFeatureToggle[] => {
 | 
			
		||||
    return [...features].sort((a, b) =>
 | 
			
		||||
        a.lastSeenAt && b.lastSeenAt
 | 
			
		||||
            ? a.lastSeenAt.localeCompare(b.lastSeenAt)
 | 
			
		||||
            : 0
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const sortByCreated = (
 | 
			
		||||
    features: Readonly<IFeatureToggle[]>
 | 
			
		||||
): IFeatureToggle[] => {
 | 
			
		||||
    return [...features].sort((a, b) =>
 | 
			
		||||
        new Date(a.createdAt) > new Date(b.createdAt) ? -1 : 1
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const sortByName = (features: Readonly<IFeatureToggle[]>): IFeatureToggle[] => {
 | 
			
		||||
    return [...features].sort((a, b) => a.name.localeCompare(b.name));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const sortByProject = (
 | 
			
		||||
    features: Readonly<IFeatureToggle[]>
 | 
			
		||||
): IFeatureToggle[] => {
 | 
			
		||||
    return [...features].sort((a, b) => a.project.localeCompare(b.project));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const sortByType = (features: Readonly<IFeatureToggle[]>): IFeatureToggle[] => {
 | 
			
		||||
    return [...features].sort((a, b) => a.type.localeCompare(b.type));
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										29
									
								
								frontend/src/hooks/useLocationSettings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/src/hooks/useLocationSettings.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
			
		||||
import { getBasePath } from '../utils/format-path';
 | 
			
		||||
import { createPersistentGlobalState } from './usePersistentGlobalState';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
export interface ILocationSettings {
 | 
			
		||||
    locale: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IUseLocationSettingsOutput {
 | 
			
		||||
    locationSettings: ILocationSettings;
 | 
			
		||||
    setLocationSettings: React.Dispatch<
 | 
			
		||||
        React.SetStateAction<ILocationSettings>
 | 
			
		||||
    >;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useLocationSettings = (): IUseLocationSettingsOutput => {
 | 
			
		||||
    const [locationSettings, setLocationSettings] = useGlobalState();
 | 
			
		||||
 | 
			
		||||
    return { locationSettings, setLocationSettings };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createInitialValue = (): ILocationSettings => {
 | 
			
		||||
    return { locale: navigator.language };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const useGlobalState = createPersistentGlobalState<ILocationSettings>(
 | 
			
		||||
    `${getBasePath()}:useLocationSettings:v1`,
 | 
			
		||||
    createInitialValue()
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										29
									
								
								frontend/src/hooks/usePersistentGlobalState.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/src/hooks/usePersistentGlobalState.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { createGlobalState } from 'react-hooks-global-state';
 | 
			
		||||
import { getLocalStorageItem, setLocalStorageItem } from '../utils/storage';
 | 
			
		||||
 | 
			
		||||
type UsePersistentGlobalState<T> = () => [
 | 
			
		||||
    value: T,
 | 
			
		||||
    setValue: React.Dispatch<React.SetStateAction<T>>
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
// Create a hook that stores global state (shared across all hook instances).
 | 
			
		||||
// The state is also persisted to localStorage and restored on page load.
 | 
			
		||||
// The localStorage state is not synced between tabs.
 | 
			
		||||
export const createPersistentGlobalState = <T extends object>(
 | 
			
		||||
    key: string,
 | 
			
		||||
    initialValue: T
 | 
			
		||||
): UsePersistentGlobalState<T> => {
 | 
			
		||||
    const container = createGlobalState<{ [key: string]: T }>({
 | 
			
		||||
        [key]: getLocalStorageItem(key) ?? initialValue,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const setGlobalState = (value: React.SetStateAction<T>) => {
 | 
			
		||||
        const prev = container.getGlobalState(key);
 | 
			
		||||
        const next = typeof value === 'function' ? value(prev) : value;
 | 
			
		||||
        container.setGlobalState(key, next);
 | 
			
		||||
        setLocalStorageItem(key, next);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return () => [container.useGlobalState(key)[0], setGlobalState];
 | 
			
		||||
};
 | 
			
		||||
@ -32,8 +32,9 @@ export interface IFeatureTogglePayload {
 | 
			
		||||
export interface IFeatureToggle {
 | 
			
		||||
    stale: boolean;
 | 
			
		||||
    archived: boolean;
 | 
			
		||||
    createdAt: Date;
 | 
			
		||||
    lastSeenAt?: Date;
 | 
			
		||||
    enabled?: boolean;
 | 
			
		||||
    createdAt: string;
 | 
			
		||||
    lastSeenAt?: string;
 | 
			
		||||
    description: string;
 | 
			
		||||
    environments: IFeatureEnvironment[];
 | 
			
		||||
    name: string;
 | 
			
		||||
@ -41,6 +42,7 @@ export interface IFeatureToggle {
 | 
			
		||||
    type: string;
 | 
			
		||||
    variants: IFeatureVariant[];
 | 
			
		||||
    impressionData: boolean;
 | 
			
		||||
    strategies?: IFeatureStrategy[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IFeatureEnvironment {
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@ export interface IFlags {
 | 
			
		||||
    C: boolean;
 | 
			
		||||
    P: boolean;
 | 
			
		||||
    E: boolean;
 | 
			
		||||
    RE: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IVersionInfo {
 | 
			
		||||
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import Archive from '../../component/archive/archive-list-container';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
const render = ({ match: { params }, history }) => <Archive name={params.name} history={history} />;
 | 
			
		||||
render.propTypes = {
 | 
			
		||||
    match: PropTypes.object,
 | 
			
		||||
    history: PropTypes.object,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default render;
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import FeatureListContainer from '../../component/feature/FeatureToggleList';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
const render = ({ history }) => <FeatureListContainer history={history} />;
 | 
			
		||||
 | 
			
		||||
render.propTypes = {
 | 
			
		||||
    history: PropTypes.object.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default render;
 | 
			
		||||
@ -1,32 +0,0 @@
 | 
			
		||||
import api from './api';
 | 
			
		||||
import { dispatchError } from '../util';
 | 
			
		||||
 | 
			
		||||
export const REVIVE_TOGGLE = 'REVIVE_TOGGLE';
 | 
			
		||||
export const RECEIVE_ARCHIVE = 'RECEIVE_ARCHIVE';
 | 
			
		||||
export const ERROR_RECEIVE_ARCHIVE = 'ERROR_RECEIVE_ARCHIVE';
 | 
			
		||||
 | 
			
		||||
const receiveArchive = json => ({
 | 
			
		||||
    type: RECEIVE_ARCHIVE,
 | 
			
		||||
    value: json.features,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const reviveToggle = archiveFeatureToggle => ({
 | 
			
		||||
    type: REVIVE_TOGGLE,
 | 
			
		||||
    value: archiveFeatureToggle,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export function revive(featureToggle) {
 | 
			
		||||
    return dispatch =>
 | 
			
		||||
        api
 | 
			
		||||
            .revive(featureToggle)
 | 
			
		||||
            .then(() => dispatch(reviveToggle(featureToggle)))
 | 
			
		||||
            .catch(dispatchError(dispatch, ERROR_RECEIVE_ARCHIVE));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function fetchArchive() {
 | 
			
		||||
    return dispatch =>
 | 
			
		||||
        api
 | 
			
		||||
            .fetchAll()
 | 
			
		||||
            .then(json => dispatch(receiveArchive(json)))
 | 
			
		||||
            .catch(dispatchError(dispatch, ERROR_RECEIVE_ARCHIVE));
 | 
			
		||||
}
 | 
			
		||||
@ -1,23 +0,0 @@
 | 
			
		||||
import { formatApiPath } from '../../utils/format-path';
 | 
			
		||||
import { throwIfNotSuccess, headers } from '../api-helper';
 | 
			
		||||
 | 
			
		||||
const URI = formatApiPath('api/admin/archive');
 | 
			
		||||
 | 
			
		||||
function fetchAll() {
 | 
			
		||||
    return fetch(`${URI}/features`, { credentials: 'include' })
 | 
			
		||||
        .then(throwIfNotSuccess)
 | 
			
		||||
        .then(response => response.json());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function revive(featureName) {
 | 
			
		||||
    return fetch(`${URI}/revive/${featureName}`, {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        headers,
 | 
			
		||||
        credentials: 'include',
 | 
			
		||||
    }).then(throwIfNotSuccess);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    fetchAll,
 | 
			
		||||
    revive,
 | 
			
		||||
};
 | 
			
		||||
@ -1,23 +0,0 @@
 | 
			
		||||
import { List, Map as $Map } from 'immutable';
 | 
			
		||||
import { RECEIVE_ARCHIVE, REVIVE_TOGGLE } from './actions';
 | 
			
		||||
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
 | 
			
		||||
 | 
			
		||||
function getInitState() {
 | 
			
		||||
    return new $Map({ list: new List() });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const archiveStore = (state = getInitState(), action) => {
 | 
			
		||||
    switch (action.type) {
 | 
			
		||||
        case REVIVE_TOGGLE:
 | 
			
		||||
            return state.update('list', list => list.filter(item => item.name !== action.value));
 | 
			
		||||
        case RECEIVE_ARCHIVE:
 | 
			
		||||
            return state.set('list', new List(action.value));
 | 
			
		||||
        case USER_LOGOUT:
 | 
			
		||||
        case USER_LOGIN:
 | 
			
		||||
            return getInitState();
 | 
			
		||||
        default:
 | 
			
		||||
            return state;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default archiveStore;
 | 
			
		||||
@ -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;
 | 
			
		||||
            });
 | 
			
		||||
}
 | 
			
		||||
@ -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,
 | 
			
		||||
};
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -6,16 +6,13 @@ import featureTags from './feature-tags';
 | 
			
		||||
import tagTypes from './tag-type';
 | 
			
		||||
import tags from './tag';
 | 
			
		||||
import strategies from './strategy';
 | 
			
		||||
import archive from './archive';
 | 
			
		||||
import error from './error';
 | 
			
		||||
import settings from './settings';
 | 
			
		||||
import user from './user';
 | 
			
		||||
import applications from './application';
 | 
			
		||||
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';
 | 
			
		||||
@ -28,16 +25,13 @@ const unleashStore = combineReducers({
 | 
			
		||||
    tagTypes,
 | 
			
		||||
    tags,
 | 
			
		||||
    featureTags,
 | 
			
		||||
    archive,
 | 
			
		||||
    error,
 | 
			
		||||
    settings,
 | 
			
		||||
    user,
 | 
			
		||||
    applications,
 | 
			
		||||
    uiConfig,
 | 
			
		||||
    context,
 | 
			
		||||
    projects,
 | 
			
		||||
    addons,
 | 
			
		||||
    authAdmin,
 | 
			
		||||
    apiCalls,
 | 
			
		||||
    invoiceAdmin,
 | 
			
		||||
    feedback,
 | 
			
		||||
 | 
			
		||||
@ -1,10 +0,0 @@
 | 
			
		||||
export const UPDATE_SETTING = 'UPDATE_SETTING';
 | 
			
		||||
 | 
			
		||||
export const updateSetting = (group, field, value) => ({
 | 
			
		||||
    type: UPDATE_SETTING,
 | 
			
		||||
    group,
 | 
			
		||||
    field,
 | 
			
		||||
    value,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const updateSettingForGroup = group => (field, value) => updateSetting(group, field, value);
 | 
			
		||||
@ -1,44 +0,0 @@
 | 
			
		||||
import { fromJS } from 'immutable';
 | 
			
		||||
import { UPDATE_SETTING } from './actions';
 | 
			
		||||
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
 | 
			
		||||
 | 
			
		||||
import { getBasePath } from '../../utils/format-path';
 | 
			
		||||
 | 
			
		||||
const localStorage = window.localStorage || {
 | 
			
		||||
    setItem: () => {},
 | 
			
		||||
    getItem: () => {},
 | 
			
		||||
};
 | 
			
		||||
const basePath =  getBasePath();
 | 
			
		||||
const SETTINGS = `${basePath}:settings`;
 | 
			
		||||
 | 
			
		||||
const DEFAULT = fromJS({ location: {} });
 | 
			
		||||
 | 
			
		||||
function getInitState() {
 | 
			
		||||
    try {
 | 
			
		||||
        const state = JSON.parse(localStorage.getItem(SETTINGS));
 | 
			
		||||
        return state ? DEFAULT.merge(state) : DEFAULT;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        return DEFAULT;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updateSetting(state, action) {
 | 
			
		||||
    const newState = state.updateIn([action.group, action.field], () => action.value);
 | 
			
		||||
 | 
			
		||||
    localStorage.setItem(SETTINGS, JSON.stringify(newState.toJSON()));
 | 
			
		||||
    return newState;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const settingStore = (state = getInitState(), action) => {
 | 
			
		||||
    switch (action.type) {
 | 
			
		||||
        case UPDATE_SETTING:
 | 
			
		||||
            return updateSetting(state, action);
 | 
			
		||||
        case USER_LOGOUT:
 | 
			
		||||
        case USER_LOGIN:
 | 
			
		||||
            return getInitState();
 | 
			
		||||
        default:
 | 
			
		||||
            return state;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default settingStore;
 | 
			
		||||
							
								
								
									
										8
									
								
								frontend/src/utils/format-unknown-error.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								frontend/src/utils/format-unknown-error.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -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');
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										10
									
								
								frontend/src/utils/format-unknown-error.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/utils/format-unknown-error.ts
									
									
									
									
									
										Normal file
									
								
							@ -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';
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										11
									
								
								frontend/src/utils/remove-empty-string-fields.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/utils/remove-empty-string-fields.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -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' });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										9
									
								
								frontend/src/utils/remove-empty-string-fields.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								frontend/src/utils/remove-empty-string-fields.ts
									
									
									
									
									
										Normal file
									
								
							@ -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);
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										29
									
								
								frontend/src/utils/storage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/src/utils/storage.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
			
		||||
// Get an item from localStorage.
 | 
			
		||||
// Returns undefined if the browser denies access.
 | 
			
		||||
export function getLocalStorageItem<T>(key: string): T | undefined {
 | 
			
		||||
    try {
 | 
			
		||||
        return parseStoredItem<T>(window.localStorage.getItem(key));
 | 
			
		||||
    } catch (err: unknown) {
 | 
			
		||||
        console.warn(err);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Store an item in localStorage.
 | 
			
		||||
// Does nothing if the browser denies access.
 | 
			
		||||
export function setLocalStorageItem(key: string, value: unknown) {
 | 
			
		||||
    try {
 | 
			
		||||
        window.localStorage.setItem(key, JSON.stringify(value));
 | 
			
		||||
    } catch (err: unknown) {
 | 
			
		||||
        console.warn(err);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Parse an item from localStorage.
 | 
			
		||||
// Returns undefined if the item could not be parsed.
 | 
			
		||||
function parseStoredItem<T>(data: string | null): T | undefined {
 | 
			
		||||
    try {
 | 
			
		||||
        return data ? JSON.parse(data) : undefined;
 | 
			
		||||
    } catch (err: unknown) {
 | 
			
		||||
        console.warn(err);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -10310,6 +10310,11 @@ react-error-overlay@^6.0.9:
 | 
			
		||||
  resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz"
 | 
			
		||||
  integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
 | 
			
		||||
 | 
			
		||||
react-hooks-global-state@^1.0.2:
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react-hooks-global-state/-/react-hooks-global-state-1.0.2.tgz#37bbc3203a0be9f3ac0658abfd28dd7ce7ee166c"
 | 
			
		||||
  integrity sha512-UcWz+VjcUUCQ7bXGmOhanGII3j22zyPSjwJnQWeycxFYj/etBxIbz9xziEm4sv5+OqGuS7bzvpx24XkCxgJ7Bg==
 | 
			
		||||
 | 
			
		||||
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6:
 | 
			
		||||
  version "16.13.1"
 | 
			
		||||
  resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user