mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	refactor: refactor addons to TSX and remove unused files (#676)
* refactor: refactor addons to TSX and remove unused files * refactor: change AddonIcon to getAddonIcon * refactor: add PermissionButton instead of conditional render * refactor: wrap icon buttons inside PermissionIconButtons * feat: add confirm delete dialog * fix: create addon form * fix: refactor addons * fix: remove addon store folder * fix: update index * fix: rebase * fix: update exports * fix: update snapshot * fix: add dev dep Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
		
							parent
							
								
									500d405fa5
								
							
						
					
					
						commit
						2a9a3ac569
					
				@ -67,6 +67,7 @@
 | 
			
		||||
    "fetch-mock": "9.11.0",
 | 
			
		||||
    "http-proxy-middleware": "2.0.2",
 | 
			
		||||
    "immutable": "4.0.0",
 | 
			
		||||
    "@types/lodash.clonedeep": "^4.5.6",
 | 
			
		||||
    "lodash.clonedeep": "4.5.0",
 | 
			
		||||
    "lodash.flow": "3.5.0",
 | 
			
		||||
    "node-fetch": "2.6.7",
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,43 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Grid, FormControlLabel, Checkbox } from '@material-ui/core';
 | 
			
		||||
 | 
			
		||||
import { styles as commonStyles } from '../../../common';
 | 
			
		||||
import { IAddonProvider } from '../../../../interfaces/addons';
 | 
			
		||||
 | 
			
		||||
interface IAddonProps {
 | 
			
		||||
    provider: IAddonProvider;
 | 
			
		||||
    checkedEvents: string[];
 | 
			
		||||
    setEventValue: (name: string) => void;
 | 
			
		||||
    error: Record<string, string>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AddonEvents = ({
 | 
			
		||||
    provider,
 | 
			
		||||
    checkedEvents,
 | 
			
		||||
    setEventValue,
 | 
			
		||||
    error,
 | 
			
		||||
}: IAddonProps) => {
 | 
			
		||||
    if (!provider) return null;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <React.Fragment>
 | 
			
		||||
            <h4>Events</h4>
 | 
			
		||||
            <span className={commonStyles.error}>{error}</span>
 | 
			
		||||
            <Grid container spacing={0}>
 | 
			
		||||
                {provider.events.map(e => (
 | 
			
		||||
                    <Grid item xs={4} key={e}>
 | 
			
		||||
                        <FormControlLabel
 | 
			
		||||
                            control={
 | 
			
		||||
                                <Checkbox
 | 
			
		||||
                                    checked={checkedEvents.includes(e)}
 | 
			
		||||
                                    onChange={setEventValue(e)}
 | 
			
		||||
                                />
 | 
			
		||||
                            }
 | 
			
		||||
                            label={e}
 | 
			
		||||
                        />
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                ))}
 | 
			
		||||
            </Grid>
 | 
			
		||||
        </React.Fragment>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -1,23 +1,29 @@
 | 
			
		||||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { TextField, FormControlLabel, Switch } from '@material-ui/core';
 | 
			
		||||
 | 
			
		||||
import { FormButtons, styles as commonStyles } from '../common';
 | 
			
		||||
import { trim } from '../common/util';
 | 
			
		||||
import AddonParameters from './form-addon-parameters';
 | 
			
		||||
import AddonEvents from './form-addon-events';
 | 
			
		||||
import { FormButtons, styles as commonStyles } from '../../common';
 | 
			
		||||
import { trim } from '../../common/util';
 | 
			
		||||
import { AddonParameters } from './AddonParameters/AddonParameters';
 | 
			
		||||
import { AddonEvents } from './AddonEvents/AddonEvents';
 | 
			
		||||
import cloneDeep from 'lodash.clonedeep';
 | 
			
		||||
 | 
			
		||||
import styles from './form-addon-component.module.scss';
 | 
			
		||||
import PageContent from '../common/PageContent/PageContent';
 | 
			
		||||
import useAddonsApi from '../../hooks/api/actions/useAddonsApi/useAddonsApi';
 | 
			
		||||
import useToast from '../../hooks/useToast';
 | 
			
		||||
import PageContent from '../../common/PageContent/PageContent';
 | 
			
		||||
import { useHistory } from 'react-router-dom';
 | 
			
		||||
import useAddonsApi from '../../../hooks/api/actions/useAddonsApi/useAddonsApi';
 | 
			
		||||
import useToast from '../../../hooks/useToast';
 | 
			
		||||
import { makeStyles } from '@material-ui/styles';
 | 
			
		||||
 | 
			
		||||
const AddonFormComponent = ({ editMode, provider, addon, fetch }) => {
 | 
			
		||||
const useStyles = makeStyles(theme => ({
 | 
			
		||||
    nameInput: {
 | 
			
		||||
        marginRight: '1.5rem',
 | 
			
		||||
    },
 | 
			
		||||
    formSection: { padding: '10px 28px' },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const AddonForm = ({ editMode, provider, addon, fetch }) => {
 | 
			
		||||
    const { createAddon, updateAddon } = useAddonsApi();
 | 
			
		||||
    const { setToastData, setToastApiError } = useToast();
 | 
			
		||||
    const history = useHistory();
 | 
			
		||||
    const styles = useStyles();
 | 
			
		||||
 | 
			
		||||
    const [config, setConfig] = useState(addon);
 | 
			
		||||
    const [errors, setErrors] = useState({
 | 
			
		||||
@ -116,6 +122,7 @@ const AddonFormComponent = ({ editMode, provider, addon, fetch }) => {
 | 
			
		||||
                history.push('/addons');
 | 
			
		||||
                setToastData({
 | 
			
		||||
                    type: 'success',
 | 
			
		||||
                    confetti: true,
 | 
			
		||||
                    title: 'Addon created successfully',
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
@ -196,14 +203,17 @@ const AddonFormComponent = ({ editMode, provider, addon, fetch }) => {
 | 
			
		||||
                    />
 | 
			
		||||
                </section>
 | 
			
		||||
                <section className={styles.formSection}>
 | 
			
		||||
                    <FormButtons submitText={submitText} onCancel={handleCancel} />
 | 
			
		||||
                    <FormButtons
 | 
			
		||||
                        submitText={submitText}
 | 
			
		||||
                        onCancel={handleCancel}
 | 
			
		||||
                    />
 | 
			
		||||
                </section>
 | 
			
		||||
            </form>
 | 
			
		||||
        </PageContent>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
AddonFormComponent.propTypes = {
 | 
			
		||||
AddonForm.propTypes = {
 | 
			
		||||
    provider: PropTypes.object,
 | 
			
		||||
    addon: PropTypes.object.isRequired,
 | 
			
		||||
    fetch: PropTypes.func.isRequired,
 | 
			
		||||
@ -211,5 +221,3 @@ AddonFormComponent.propTypes = {
 | 
			
		||||
    cancel: PropTypes.func.isRequired,
 | 
			
		||||
    editMode: PropTypes.bool.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default AddonFormComponent;
 | 
			
		||||
@ -0,0 +1,60 @@
 | 
			
		||||
import { TextField } from '@material-ui/core';
 | 
			
		||||
import {
 | 
			
		||||
    IAddonConfig,
 | 
			
		||||
    IAddonProvider,
 | 
			
		||||
    IAddonProviderParams,
 | 
			
		||||
} from '../../../../../interfaces/addons';
 | 
			
		||||
 | 
			
		||||
const resolveType = ({ type = 'text', sensitive = false }, value: string) => {
 | 
			
		||||
    if (sensitive && value === MASKED_VALUE) {
 | 
			
		||||
        return 'text';
 | 
			
		||||
    }
 | 
			
		||||
    if (type === 'textfield') {
 | 
			
		||||
        return 'text';
 | 
			
		||||
    }
 | 
			
		||||
    return type;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const MASKED_VALUE = '*****';
 | 
			
		||||
 | 
			
		||||
interface IAddonParameterProps {
 | 
			
		||||
    provider: IAddonProvider;
 | 
			
		||||
    errors: Record<string, string>;
 | 
			
		||||
    definition: IAddonProviderParams;
 | 
			
		||||
    setParameterValue: (param: string) => void;
 | 
			
		||||
    config: IAddonConfig;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AddonParameter = ({
 | 
			
		||||
    definition,
 | 
			
		||||
    config,
 | 
			
		||||
    errors,
 | 
			
		||||
    setParameterValue,
 | 
			
		||||
}: IAddonParameterProps) => {
 | 
			
		||||
    const value = config.parameters[definition.name] || '';
 | 
			
		||||
    const type = resolveType(definition, value);
 | 
			
		||||
    const error = errors.parameters[definition.name];
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div style={{ width: '80%', marginTop: '25px' }}>
 | 
			
		||||
            <TextField
 | 
			
		||||
                size="small"
 | 
			
		||||
                style={{ width: '100%' }}
 | 
			
		||||
                rows={definition.type === 'textfield' ? 9 : 0}
 | 
			
		||||
                multiline={definition.type === 'textfield'}
 | 
			
		||||
                type={type}
 | 
			
		||||
                label={definition.displayName}
 | 
			
		||||
                name={definition.name}
 | 
			
		||||
                placeholder={definition.placeholder || ''}
 | 
			
		||||
                InputLabelProps={{
 | 
			
		||||
                    shrink: true,
 | 
			
		||||
                }}
 | 
			
		||||
                value={value}
 | 
			
		||||
                error={error}
 | 
			
		||||
                onChange={setParameterValue(definition.name)}
 | 
			
		||||
                variant="outlined"
 | 
			
		||||
                helperText={definition.description}
 | 
			
		||||
            />
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,43 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { IAddonConfig, IAddonProvider } from '../../../../interfaces/addons';
 | 
			
		||||
import { AddonParameter } from './AddonParameter/AddonParameter';
 | 
			
		||||
 | 
			
		||||
interface IAddonParametersProps {
 | 
			
		||||
    provider: IAddonProvider;
 | 
			
		||||
    errors: Record<string, string>;
 | 
			
		||||
    editMode: boolean;
 | 
			
		||||
    setParameterValue: (param: string) => void;
 | 
			
		||||
    config: IAddonConfig;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AddonParameters = ({
 | 
			
		||||
    provider,
 | 
			
		||||
    config,
 | 
			
		||||
    errors,
 | 
			
		||||
    setParameterValue,
 | 
			
		||||
    editMode,
 | 
			
		||||
}: IAddonParametersProps) => {
 | 
			
		||||
    if (!provider) return null;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <React.Fragment>
 | 
			
		||||
            <h4>Parameters</h4>
 | 
			
		||||
            {editMode ? (
 | 
			
		||||
                <p>
 | 
			
		||||
                    Sensitive parameters will be masked with value "<i>*****</i>
 | 
			
		||||
                    ". If you don't change the value they will not be updated
 | 
			
		||||
                    when saving.
 | 
			
		||||
                </p>
 | 
			
		||||
            ) : null}
 | 
			
		||||
            {provider.parameters.map(parameter => (
 | 
			
		||||
                <AddonParameter
 | 
			
		||||
                    key={parameter.name}
 | 
			
		||||
                    definition={parameter}
 | 
			
		||||
                    errors={errors}
 | 
			
		||||
                    config={config}
 | 
			
		||||
                    setParameterValue={setParameterValue}
 | 
			
		||||
                />
 | 
			
		||||
            ))}
 | 
			
		||||
        </React.Fragment>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -1,12 +1,9 @@
 | 
			
		||||
import { useContext, useEffect } from 'react';
 | 
			
		||||
import ConfiguredAddons from './ConfiguredAddons';
 | 
			
		||||
import AvailableAddons from './AvailableAddons';
 | 
			
		||||
import { ReactElement } from 'react';
 | 
			
		||||
import { ConfiguredAddons } from './ConfiguredAddons/ConfiguredAddons';
 | 
			
		||||
import { AvailableAddons } from './AvailableAddons/AvailableAddons';
 | 
			
		||||
import { Avatar } from '@material-ui/core';
 | 
			
		||||
import { DeviceHub } from '@material-ui/icons';
 | 
			
		||||
 | 
			
		||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import AccessContext from '../../../contexts/AccessContext';
 | 
			
		||||
 | 
			
		||||
import slackIcon from '../../../assets/icons/slack.svg';
 | 
			
		||||
import jiraIcon from '../../../assets/icons/jira.svg';
 | 
			
		||||
import webhooksIcon from '../../../assets/icons/webhooks.svg';
 | 
			
		||||
@ -14,7 +11,6 @@ import teamsIcon from '../../../assets/icons/teams.svg';
 | 
			
		||||
import dataDogIcon from '../../../assets/icons/datadog.svg';
 | 
			
		||||
import { formatAssetPath } from '../../../utils/format-path';
 | 
			
		||||
import useAddons from '../../../hooks/api/getters/useAddons/useAddons';
 | 
			
		||||
import { useHistory } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
const style = {
 | 
			
		||||
    width: '40px',
 | 
			
		||||
@ -23,7 +19,7 @@ const style = {
 | 
			
		||||
    float: 'left',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getIcon = name => {
 | 
			
		||||
const getAddonIcon = (name: string): ReactElement => {
 | 
			
		||||
    switch (name) {
 | 
			
		||||
        case 'slack':
 | 
			
		||||
            return (
 | 
			
		||||
@ -74,40 +70,21 @@ const getIcon = name => {
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const AddonList = () => {
 | 
			
		||||
    const { hasAccess } = useContext(AccessContext);
 | 
			
		||||
    const { addons, providers, refetchAddons } = useAddons();
 | 
			
		||||
    const history = useHistory();
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (addons.length === 0) {
 | 
			
		||||
            refetchAddons();
 | 
			
		||||
        }
 | 
			
		||||
        // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
    }, [addons.length]);
 | 
			
		||||
export const AddonList = () => {
 | 
			
		||||
    const { providers, addons } = useAddons();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <ConditionallyRender
 | 
			
		||||
                condition={addons.length > 0}
 | 
			
		||||
                show={
 | 
			
		||||
                    <ConfiguredAddons
 | 
			
		||||
                        addons={addons}
 | 
			
		||||
                        hasAccess={hasAccess}
 | 
			
		||||
                        getIcon={getIcon}
 | 
			
		||||
                    />
 | 
			
		||||
                }
 | 
			
		||||
                show={<ConfiguredAddons getAddonIcon={getAddonIcon} />}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <br />
 | 
			
		||||
            <AvailableAddons
 | 
			
		||||
                providers={providers}
 | 
			
		||||
                hasAccess={hasAccess}
 | 
			
		||||
                history={history}
 | 
			
		||||
                getIcon={getIcon}
 | 
			
		||||
                getAddonIcon={getAddonIcon}
 | 
			
		||||
            />
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default AddonList;
 | 
			
		||||
@ -1,57 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PageContent from '../../../common/PageContent/PageContent';
 | 
			
		||||
import {
 | 
			
		||||
    Button,
 | 
			
		||||
    List,
 | 
			
		||||
    ListItem,
 | 
			
		||||
    ListItemAvatar,
 | 
			
		||||
    ListItemSecondaryAction,
 | 
			
		||||
    ListItemText,
 | 
			
		||||
} from '@material-ui/core';
 | 
			
		||||
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import { CREATE_ADDON } from '../../../providers/AccessProvider/permissions';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
const AvailableAddons = ({ providers, getIcon, hasAccess, history }) => {
 | 
			
		||||
    
 | 
			
		||||
    const renderProvider = provider => (
 | 
			
		||||
        <ListItem key={provider.name}>
 | 
			
		||||
            <ListItemAvatar>{getIcon(provider.name)}</ListItemAvatar>
 | 
			
		||||
            <ListItemText
 | 
			
		||||
                primary={provider.displayName}
 | 
			
		||||
                secondary={provider.description}
 | 
			
		||||
            />
 | 
			
		||||
            <ListItemSecondaryAction>
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={hasAccess(CREATE_ADDON)}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <Button
 | 
			
		||||
                            variant="contained"
 | 
			
		||||
                            name="device_hub"
 | 
			
		||||
                            onClick={() =>
 | 
			
		||||
                                history.push(`/addons/create/${provider.name}`)
 | 
			
		||||
                            }
 | 
			
		||||
                            title="Configure"
 | 
			
		||||
                        >
 | 
			
		||||
                            Configure
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
            </ListItemSecondaryAction>
 | 
			
		||||
        </ListItem>
 | 
			
		||||
    );
 | 
			
		||||
    return (
 | 
			
		||||
        <PageContent headerContent="Available addons">
 | 
			
		||||
            <List>{providers.map(provider => renderProvider(provider))}</List>
 | 
			
		||||
        </PageContent>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
AvailableAddons.propTypes = {
 | 
			
		||||
    providers: PropTypes.array.isRequired,
 | 
			
		||||
    getIcon: PropTypes.func.isRequired,
 | 
			
		||||
    hasAccess: PropTypes.func.isRequired,
 | 
			
		||||
    history: PropTypes.object.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default AvailableAddons;
 | 
			
		||||
@ -0,0 +1,63 @@
 | 
			
		||||
import { ReactElement } from 'react';
 | 
			
		||||
import PageContent from '../../../common/PageContent/PageContent';
 | 
			
		||||
import {
 | 
			
		||||
    List,
 | 
			
		||||
    ListItem,
 | 
			
		||||
    ListItemAvatar,
 | 
			
		||||
    ListItemSecondaryAction,
 | 
			
		||||
    ListItemText,
 | 
			
		||||
} from '@material-ui/core';
 | 
			
		||||
import { CREATE_ADDON } from '../../../providers/AccessProvider/permissions';
 | 
			
		||||
import { useHistory } from 'react-router-dom';
 | 
			
		||||
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
 | 
			
		||||
 | 
			
		||||
interface IProvider {
 | 
			
		||||
    name: string;
 | 
			
		||||
    displayName: string;
 | 
			
		||||
    description: string;
 | 
			
		||||
    documentationUrl: string;
 | 
			
		||||
    parameters: object[];
 | 
			
		||||
    events: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IAvailableAddonsProps {
 | 
			
		||||
    getAddonIcon: (name: string) => ReactElement;
 | 
			
		||||
    providers: IProvider[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AvailableAddons = ({
 | 
			
		||||
    providers,
 | 
			
		||||
    getAddonIcon,
 | 
			
		||||
}: IAvailableAddonsProps) => {
 | 
			
		||||
    const history = useHistory();
 | 
			
		||||
 | 
			
		||||
    const renderProvider = (provider: IProvider) => (
 | 
			
		||||
        <ListItem key={provider.name}>
 | 
			
		||||
            <ListItemAvatar>{getAddonIcon(provider.name)}</ListItemAvatar>
 | 
			
		||||
            <ListItemText
 | 
			
		||||
                primary={provider.displayName}
 | 
			
		||||
                secondary={provider.description}
 | 
			
		||||
            />
 | 
			
		||||
            <ListItemSecondaryAction>
 | 
			
		||||
                <PermissionButton
 | 
			
		||||
                    permission={CREATE_ADDON}
 | 
			
		||||
                    onClick={() =>
 | 
			
		||||
                        history.push(`/addons/create/${provider.name}`)
 | 
			
		||||
                    }
 | 
			
		||||
                    tooltip={`Configure ${provider.name} Addon`}
 | 
			
		||||
                >
 | 
			
		||||
                    Configure
 | 
			
		||||
                </PermissionButton>
 | 
			
		||||
            </ListItemSecondaryAction>
 | 
			
		||||
        </ListItem>
 | 
			
		||||
    );
 | 
			
		||||
    return (
 | 
			
		||||
        <PageContent headerContent="Available addons">
 | 
			
		||||
            <List>
 | 
			
		||||
                {providers.map((provider: IProvider) =>
 | 
			
		||||
                    renderProvider(provider)
 | 
			
		||||
                )}
 | 
			
		||||
            </List>
 | 
			
		||||
        </PageContent>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
import AvailableAddons from './AvailableAddons';
 | 
			
		||||
 | 
			
		||||
export default AvailableAddons;
 | 
			
		||||
@ -1,6 +1,4 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import {
 | 
			
		||||
    IconButton,
 | 
			
		||||
    List,
 | 
			
		||||
    ListItem,
 | 
			
		||||
    ListItemAvatar,
 | 
			
		||||
@ -8,7 +6,6 @@ import {
 | 
			
		||||
    ListItemText,
 | 
			
		||||
} from '@material-ui/core';
 | 
			
		||||
import { Visibility, VisibilityOff, Delete } from '@material-ui/icons';
 | 
			
		||||
 | 
			
		||||
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import {
 | 
			
		||||
    DELETE_ADDON,
 | 
			
		||||
@ -16,17 +13,43 @@ import {
 | 
			
		||||
} from '../../../providers/AccessProvider/permissions';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import PageContent from '../../../common/PageContent/PageContent';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import useAddons from '../../../../hooks/api/getters/useAddons/useAddons';
 | 
			
		||||
import useToast from '../../../../hooks/useToast';
 | 
			
		||||
import useAddonsApi from '../../../../hooks/api/actions/useAddonsApi/useAddonsApi';
 | 
			
		||||
import { ReactElement, useContext, useState } from 'react';
 | 
			
		||||
import AccessContext from '../../../../contexts/AccessContext';
 | 
			
		||||
import { IAddon } from '../../../../interfaces/addons';
 | 
			
		||||
import PermissionIconButton from '../../../common/PermissionIconButton/PermissionIconButton';
 | 
			
		||||
import Dialogue from '../../../common/Dialogue';
 | 
			
		||||
 | 
			
		||||
const ConfiguredAddons = ({ addons, hasAccess, getIcon }) => {
 | 
			
		||||
    const { refetchAddons } = useAddons();
 | 
			
		||||
interface IConfigureAddonsProps {
 | 
			
		||||
    getAddonIcon: (name: string) => ReactElement;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => {
 | 
			
		||||
    const { refetchAddons, addons } = useAddons();
 | 
			
		||||
    const { updateAddon, removeAddon } = useAddonsApi();
 | 
			
		||||
    const { setToastData, setToastApiError } = useToast();
 | 
			
		||||
    const { hasAccess } = useContext(AccessContext);
 | 
			
		||||
    const [showDelete, setShowDelete] = useState(false);
 | 
			
		||||
    const [deletedAddon, setDeletedAddon] = useState<IAddon>({
 | 
			
		||||
        id: 0,
 | 
			
		||||
        provider: '',
 | 
			
		||||
        description: '',
 | 
			
		||||
        enabled: false,
 | 
			
		||||
        events: [],
 | 
			
		||||
        parameters: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const toggleAddon = async addon => {
 | 
			
		||||
    const sortAddons = (addons: IAddon[]) => {
 | 
			
		||||
        if (!addons) return [];
 | 
			
		||||
 | 
			
		||||
        return addons.sort((addonA: IAddon, addonB: IAddon) => {
 | 
			
		||||
            return addonA.id - addonB.id;
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const toggleAddon = async (addon: IAddon) => {
 | 
			
		||||
        try {
 | 
			
		||||
            await updateAddon({ ...addon, enabled: !addon.enabled });
 | 
			
		||||
            refetchAddons();
 | 
			
		||||
@ -35,12 +58,12 @@ const ConfiguredAddons = ({ addons, hasAccess, getIcon }) => {
 | 
			
		||||
                title: 'Success',
 | 
			
		||||
                text: 'Addon state switched successfully',
 | 
			
		||||
            });
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
        } catch (e: any) {
 | 
			
		||||
            setToastApiError(e.toString());
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onRemoveAddon = addon => async () => {
 | 
			
		||||
    const onRemoveAddon = async (addon: IAddon) => {
 | 
			
		||||
        try {
 | 
			
		||||
            await removeAddon(addon.id);
 | 
			
		||||
            refetchAddons();
 | 
			
		||||
@ -58,9 +81,9 @@ const ConfiguredAddons = ({ addons, hasAccess, getIcon }) => {
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const renderAddon = addon => (
 | 
			
		||||
    const renderAddon = (addon: IAddon) => (
 | 
			
		||||
        <ListItem key={addon.id}>
 | 
			
		||||
            <ListItemAvatar>{getIcon(addon.provider)}</ListItemAvatar>
 | 
			
		||||
            <ListItemAvatar>{getAddonIcon(addon.provider)}</ListItemAvatar>
 | 
			
		||||
            <ListItemText
 | 
			
		||||
                primary={
 | 
			
		||||
                    <span>
 | 
			
		||||
@ -85,14 +108,9 @@ const ConfiguredAddons = ({ addons, hasAccess, getIcon }) => {
 | 
			
		||||
                secondary={addon.description}
 | 
			
		||||
            />
 | 
			
		||||
            <ListItemSecondaryAction>
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={hasAccess(UPDATE_ADDON)}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <IconButton
 | 
			
		||||
                            size="small"
 | 
			
		||||
                            title={
 | 
			
		||||
                                addon.enabled ? 'Disable addon' : 'Enable addon'
 | 
			
		||||
                            }
 | 
			
		||||
                <PermissionIconButton
 | 
			
		||||
                    permission={UPDATE_ADDON}
 | 
			
		||||
                    tooltip={addon.enabled ? 'Disable addon' : 'Enable addon'}
 | 
			
		||||
                    onClick={() => toggleAddon(addon)}
 | 
			
		||||
                >
 | 
			
		||||
                    <ConditionallyRender
 | 
			
		||||
@ -100,35 +118,38 @@ const ConfiguredAddons = ({ addons, hasAccess, getIcon }) => {
 | 
			
		||||
                        show={<Visibility />}
 | 
			
		||||
                        elseShow={<VisibilityOff />}
 | 
			
		||||
                    />
 | 
			
		||||
                        </IconButton>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={hasAccess(DELETE_ADDON)}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <IconButton
 | 
			
		||||
                            size="small"
 | 
			
		||||
                            title="Remove addon"
 | 
			
		||||
                            onClick={onRemoveAddon(addon)}
 | 
			
		||||
                </PermissionIconButton>
 | 
			
		||||
                <PermissionIconButton
 | 
			
		||||
                    permission={DELETE_ADDON}
 | 
			
		||||
                    tooltip={'Remove Addon'}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                        setDeletedAddon(addon);
 | 
			
		||||
                        setShowDelete(true);
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    <Delete />
 | 
			
		||||
                        </IconButton>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
                </PermissionIconButton>
 | 
			
		||||
            </ListItemSecondaryAction>
 | 
			
		||||
        </ListItem>
 | 
			
		||||
    );
 | 
			
		||||
    return (
 | 
			
		||||
        <PageContent headerContent="Configured addons">
 | 
			
		||||
            <List>{addons.map(addon => renderAddon(addon))}</List>
 | 
			
		||||
            <List>
 | 
			
		||||
                {sortAddons(addons).map((addon: IAddon) => renderAddon(addon))}
 | 
			
		||||
            </List>
 | 
			
		||||
            <Dialogue
 | 
			
		||||
                open={showDelete}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                    onRemoveAddon(deletedAddon);
 | 
			
		||||
                    setShowDelete(false);
 | 
			
		||||
                }}
 | 
			
		||||
                onClose={() => {
 | 
			
		||||
                    setShowDelete(false);
 | 
			
		||||
                }}
 | 
			
		||||
                title="Confirm deletion"
 | 
			
		||||
            >
 | 
			
		||||
                <div>Are you sure you want to delete this Addon?</div>
 | 
			
		||||
            </Dialogue>
 | 
			
		||||
        </PageContent>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
ConfiguredAddons.propTypes = {
 | 
			
		||||
    addons: PropTypes.array.isRequired,
 | 
			
		||||
    hasAccess: PropTypes.func.isRequired,
 | 
			
		||||
    toggleAddon: PropTypes.func.isRequired,
 | 
			
		||||
    getIcon: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ConfiguredAddons;
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
import ConfiguredAddons from './ConfiguredAddons';
 | 
			
		||||
 | 
			
		||||
export default ConfiguredAddons;
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
import AddonListComponent from './AddonList';
 | 
			
		||||
 | 
			
		||||
export default AddonListComponent;
 | 
			
		||||
							
								
								
									
										42
									
								
								frontend/src/component/addons/CreateAddon/CreateAddon.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								frontend/src/component/addons/CreateAddon/CreateAddon.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
			
		||||
import { useParams } from 'react-router-dom';
 | 
			
		||||
import useAddons from '../../../hooks/api/getters/useAddons/useAddons';
 | 
			
		||||
import { AddonForm } from '../AddonForm/AddonForm';
 | 
			
		||||
import cloneDeep from 'lodash.clonedeep';
 | 
			
		||||
 | 
			
		||||
interface IAddonCreateParams {
 | 
			
		||||
    providerId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DEFAULT_DATA = {
 | 
			
		||||
    provider: '',
 | 
			
		||||
    description: '',
 | 
			
		||||
    enabled: true,
 | 
			
		||||
    parameters: {},
 | 
			
		||||
    events: [],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const CreateAddon = () => {
 | 
			
		||||
    const { providerId } = useParams<IAddonCreateParams>();
 | 
			
		||||
 | 
			
		||||
    const { providers, refetchAddons } = useAddons();
 | 
			
		||||
 | 
			
		||||
    const editMode = false;
 | 
			
		||||
    const provider = providers.find(
 | 
			
		||||
        (providerItem: any) => providerItem.name === providerId
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const defaultAddon = {
 | 
			
		||||
        ...cloneDeep(DEFAULT_DATA),
 | 
			
		||||
        provider: provider ? provider.name : '',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        // @ts-expect-error
 | 
			
		||||
        <AddonForm
 | 
			
		||||
            editMode={editMode}
 | 
			
		||||
            provider={provider}
 | 
			
		||||
            fetch={refetchAddons}
 | 
			
		||||
            addon={defaultAddon}
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										41
									
								
								frontend/src/component/addons/EditAddon/EditAddon.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								frontend/src/component/addons/EditAddon/EditAddon.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
import { useParams } from 'react-router-dom';
 | 
			
		||||
import useAddons from '../../../hooks/api/getters/useAddons/useAddons';
 | 
			
		||||
import { AddonForm } from '../AddonForm/AddonForm';
 | 
			
		||||
import cloneDeep from 'lodash.clonedeep';
 | 
			
		||||
import { IAddon } from '../../../interfaces/addons';
 | 
			
		||||
 | 
			
		||||
interface IAddonEditParams {
 | 
			
		||||
    addonId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DEFAULT_DATA = {
 | 
			
		||||
    provider: '',
 | 
			
		||||
    description: '',
 | 
			
		||||
    enabled: true,
 | 
			
		||||
    parameters: {},
 | 
			
		||||
    events: [],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const EditAddon = () => {
 | 
			
		||||
    const { addonId } = useParams<IAddonEditParams>();
 | 
			
		||||
 | 
			
		||||
    const { providers, addons, refetchAddons } = useAddons();
 | 
			
		||||
 | 
			
		||||
    const editMode = true;
 | 
			
		||||
    const addon = addons.find(
 | 
			
		||||
        (addon: IAddon) => addon.id === Number(addonId)
 | 
			
		||||
    ) || { ...cloneDeep(DEFAULT_DATA) };
 | 
			
		||||
    const provider = addon
 | 
			
		||||
        ? providers.find(provider => provider.name === addon.provider)
 | 
			
		||||
        : undefined;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        // @ts-expect-error
 | 
			
		||||
        <AddonForm
 | 
			
		||||
            editMode={editMode}
 | 
			
		||||
            provider={provider}
 | 
			
		||||
            fetch={refetchAddons}
 | 
			
		||||
            addon={addon}
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
.nameInput {
 | 
			
		||||
    margin-right: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.formContainer {
 | 
			
		||||
    margin-bottom: 1.5rem;
 | 
			
		||||
    max-width: 350px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.formSection {
 | 
			
		||||
    padding: 10px 28px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.header {
 | 
			
		||||
    font-size: var(--h1-size);
 | 
			
		||||
    padding: var(--card-header-padding);
 | 
			
		||||
}
 | 
			
		||||
@ -1,55 +0,0 @@
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import FormComponent from './form-addon-component';
 | 
			
		||||
import { updateAddon, createAddon, fetchAddons } from '../../store/addons/actions';
 | 
			
		||||
import cloneDeep from 'lodash.clonedeep';
 | 
			
		||||
 | 
			
		||||
// Required for to fill the initial form.
 | 
			
		||||
const DEFAULT_DATA = {
 | 
			
		||||
    provider: '',
 | 
			
		||||
    description: '',
 | 
			
		||||
    enabled: true,
 | 
			
		||||
    parameters: {},
 | 
			
		||||
    events: [],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = (state, params) => {
 | 
			
		||||
    const defaultAddon = cloneDeep(DEFAULT_DATA);
 | 
			
		||||
    const editMode = !!params.addonId;
 | 
			
		||||
    const addons = state.addons.get('addons').toJS();
 | 
			
		||||
    const providers = state.addons.get('providers').toJS();
 | 
			
		||||
 | 
			
		||||
    let addon;
 | 
			
		||||
    let provider;
 | 
			
		||||
 | 
			
		||||
    if (editMode) {
 | 
			
		||||
        addon = addons.find(addon => addon.id === +params.addonId) || defaultAddon;
 | 
			
		||||
        provider = addon ? providers.find(provider => provider.name === addon.provider) : undefined;
 | 
			
		||||
    } else {
 | 
			
		||||
        provider = providers.find(provider => provider.name === params.provider);
 | 
			
		||||
        addon = { ...defaultAddon, provider: provider ? provider.name : '' };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        provider,
 | 
			
		||||
        addon,
 | 
			
		||||
        editMode,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mapDispatchToProps = (dispatch, ownProps) => {
 | 
			
		||||
    const { addonId, history } = ownProps;
 | 
			
		||||
    const submit = addonId ? updateAddon : createAddon;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        submit: async addonConfig => {
 | 
			
		||||
            await submit(addonConfig)(dispatch);
 | 
			
		||||
            history.push('/addons');
 | 
			
		||||
        },
 | 
			
		||||
        fetch: () => fetchAddons()(dispatch),
 | 
			
		||||
        cancel: () => history.push('/addons'),
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(FormComponent);
 | 
			
		||||
 | 
			
		||||
export default FormAddContainer;
 | 
			
		||||
@ -1,35 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { Grid, FormControlLabel, Checkbox } from '@material-ui/core';
 | 
			
		||||
 | 
			
		||||
import { styles as commonStyles } from '../common';
 | 
			
		||||
 | 
			
		||||
const AddonEvents = ({ provider, checkedEvents, setEventValue, error }) => {
 | 
			
		||||
    if (!provider) return null;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <React.Fragment>
 | 
			
		||||
            <h4>Events</h4>
 | 
			
		||||
            <span className={commonStyles.error}>{error}</span>
 | 
			
		||||
            <Grid container spacing={0}>
 | 
			
		||||
                {provider.events.map(e => (
 | 
			
		||||
                    <Grid item xs={4} key={e}>
 | 
			
		||||
                        <FormControlLabel
 | 
			
		||||
                            control={<Checkbox checked={checkedEvents.includes(e)} onChange={setEventValue(e)} />}
 | 
			
		||||
                            label={e}
 | 
			
		||||
                        />
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                ))}
 | 
			
		||||
            </Grid>
 | 
			
		||||
        </React.Fragment>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
AddonEvents.propTypes = {
 | 
			
		||||
    provider: PropTypes.object,
 | 
			
		||||
    checkedEvents: PropTypes.array.isRequired,
 | 
			
		||||
    setEventValue: PropTypes.func.isRequired,
 | 
			
		||||
    error: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default AddonEvents;
 | 
			
		||||
@ -1,86 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { TextField } from '@material-ui/core';
 | 
			
		||||
 | 
			
		||||
const MASKED_VALUE = '*****';
 | 
			
		||||
 | 
			
		||||
const resolveType = ({ type = 'text', sensitive = false }, value) => {
 | 
			
		||||
    if (sensitive && value === MASKED_VALUE) {
 | 
			
		||||
        return 'text';
 | 
			
		||||
    }
 | 
			
		||||
    if (type === 'textfield') {
 | 
			
		||||
        return 'text';
 | 
			
		||||
    }
 | 
			
		||||
    return type;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const AddonParameter = ({ definition, config, errors, setParameterValue }) => {
 | 
			
		||||
    const value = config.parameters[definition.name] || '';
 | 
			
		||||
    const type = resolveType(definition, value);
 | 
			
		||||
    const error = errors.parameters[definition.name];
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div style={{ width: '80%', marginTop: '25px' }}>
 | 
			
		||||
            <TextField
 | 
			
		||||
                size="small"
 | 
			
		||||
                style={{ width: '100%' }}
 | 
			
		||||
                rows={definition.type === 'textfield' ? 9 : 0}
 | 
			
		||||
                multiline={definition.type === 'textfield'}
 | 
			
		||||
                type={type}
 | 
			
		||||
                label={definition.displayName}
 | 
			
		||||
                name={definition.name}
 | 
			
		||||
                placeholder={definition.placeholder || ''}
 | 
			
		||||
                InputLabelProps={{
 | 
			
		||||
                    shrink: true,
 | 
			
		||||
                }}
 | 
			
		||||
                value={value}
 | 
			
		||||
                error={error}
 | 
			
		||||
                onChange={setParameterValue(definition.name)}
 | 
			
		||||
                variant="outlined"
 | 
			
		||||
                helperText={definition.description}
 | 
			
		||||
            />
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
AddonParameter.propTypes = {
 | 
			
		||||
    definition: PropTypes.object.isRequired,
 | 
			
		||||
    config: PropTypes.object.isRequired,
 | 
			
		||||
    errors: PropTypes.object.isRequired,
 | 
			
		||||
    setParameterValue: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const AddonParameters = ({ provider, config, errors, setParameterValue, editMode }) => {
 | 
			
		||||
    if (!provider) return null;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <React.Fragment>
 | 
			
		||||
            <h4>Parameters</h4>
 | 
			
		||||
            {editMode ? (
 | 
			
		||||
                <p>
 | 
			
		||||
                    Sensitive parameters will be masked with value "<i>*****</i>
 | 
			
		||||
                    ". If you don't change the value they will not be updated when saving.
 | 
			
		||||
                </p>
 | 
			
		||||
            ) : null}
 | 
			
		||||
            {provider.parameters.map(p => (
 | 
			
		||||
                <AddonParameter
 | 
			
		||||
                    key={p.name}
 | 
			
		||||
                    definition={p}
 | 
			
		||||
                    errors={errors}
 | 
			
		||||
                    config={config}
 | 
			
		||||
                    setParameterValue={setParameterValue}
 | 
			
		||||
                />
 | 
			
		||||
            ))}
 | 
			
		||||
        </React.Fragment>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
AddonParameters.propTypes = {
 | 
			
		||||
    provider: PropTypes.object,
 | 
			
		||||
    config: PropTypes.object.isRequired,
 | 
			
		||||
    errors: PropTypes.object.isRequired,
 | 
			
		||||
    setParameterValue: PropTypes.func.isRequired,
 | 
			
		||||
    editMode: PropTypes.bool,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default AddonParameters;
 | 
			
		||||
@ -1,30 +0,0 @@
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import AddonsListComponent from './AddonList';
 | 
			
		||||
import { fetchAddons, removeAddon, updateAddon } from '../../store/addons/actions';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => {
 | 
			
		||||
    const list = state.addons.toJS();
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        addons: list.addons,
 | 
			
		||||
        providers: list.providers,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mapDispatchToProps = dispatch => ({
 | 
			
		||||
    removeAddon: addon => {
 | 
			
		||||
        // eslint-disable-next-line no-alert
 | 
			
		||||
        if (window.confirm('Are you sure you want to remove this addon?')) {
 | 
			
		||||
            removeAddon(addon)(dispatch);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    fetchAddons: () => fetchAddons()(dispatch),
 | 
			
		||||
    toggleAddon: addon => {
 | 
			
		||||
        const updatedAddon = { ...addon, enabled: !addon.enabled };
 | 
			
		||||
        return updateAddon(updatedAddon)(dispatch);
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const AddonsListContainer = connect(mapStateToProps, mapDispatchToProps)(AddonsListComponent);
 | 
			
		||||
 | 
			
		||||
export default AddonsListContainer;
 | 
			
		||||
@ -286,7 +286,7 @@ Array [
 | 
			
		||||
    "layout": "main",
 | 
			
		||||
    "menu": Object {},
 | 
			
		||||
    "parent": "/addons",
 | 
			
		||||
    "path": "/addons/create/:provider",
 | 
			
		||||
    "path": "/addons/create/:providerId",
 | 
			
		||||
    "title": "Create",
 | 
			
		||||
    "type": "protected",
 | 
			
		||||
  },
 | 
			
		||||
@ -295,7 +295,7 @@ Array [
 | 
			
		||||
    "layout": "main",
 | 
			
		||||
    "menu": Object {},
 | 
			
		||||
    "parent": "/addons",
 | 
			
		||||
    "path": "/addons/edit/:id",
 | 
			
		||||
    "path": "/addons/edit/:addonId",
 | 
			
		||||
    "title": "Edit",
 | 
			
		||||
    "type": "protected",
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@ -9,9 +9,7 @@ import { ArchiveListContainer } from '../archive/ArchiveListContainer';
 | 
			
		||||
import Applications from '../../page/applications';
 | 
			
		||||
import ApplicationView from '../../page/applications/view';
 | 
			
		||||
import { TagTypeList } from '../tags/TagTypeList/TagTypeList';
 | 
			
		||||
import Addons from '../../page/addons';
 | 
			
		||||
import AddonsCreate from '../../page/addons/create';
 | 
			
		||||
import AddonsEdit from '../../page/addons/edit';
 | 
			
		||||
import { AddonList } from '../addons/AddonList/AddonList';
 | 
			
		||||
import Admin from '../admin';
 | 
			
		||||
import AdminApi from '../admin/api';
 | 
			
		||||
import AdminInvoice from '../admin/invoice/InvoiceAdminPage';
 | 
			
		||||
@ -45,6 +43,8 @@ import CreateFeature from '../feature/CreateFeature/CreateFeature';
 | 
			
		||||
import EditFeature from '../feature/EditFeature/EditFeature';
 | 
			
		||||
import ContextList from '../context/ContextList/ContextList';
 | 
			
		||||
import RedirectFeatureView from '../feature/RedirectFeatureView/RedirectFeatureView';
 | 
			
		||||
import { CreateAddon } from '../addons/CreateAddon/CreateAddon';
 | 
			
		||||
import { EditAddon } from '../addons/EditAddon/EditAddon';
 | 
			
		||||
 | 
			
		||||
export const routes = [
 | 
			
		||||
    // Project
 | 
			
		||||
@ -322,19 +322,19 @@ export const routes = [
 | 
			
		||||
 | 
			
		||||
    // Addons
 | 
			
		||||
    {
 | 
			
		||||
        path: '/addons/create/:provider',
 | 
			
		||||
        path: '/addons/create/:providerId',
 | 
			
		||||
        parent: '/addons',
 | 
			
		||||
        title: 'Create',
 | 
			
		||||
        component: AddonsCreate,
 | 
			
		||||
        component: CreateAddon,
 | 
			
		||||
        type: 'protected',
 | 
			
		||||
        layout: 'main',
 | 
			
		||||
        menu: {},
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: '/addons/edit/:id',
 | 
			
		||||
        path: '/addons/edit/:addonId',
 | 
			
		||||
        parent: '/addons',
 | 
			
		||||
        title: 'Edit',
 | 
			
		||||
        component: AddonsEdit,
 | 
			
		||||
        component: EditAddon,
 | 
			
		||||
        type: 'protected',
 | 
			
		||||
        layout: 'main',
 | 
			
		||||
        menu: {},
 | 
			
		||||
@ -342,7 +342,7 @@ export const routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '/addons',
 | 
			
		||||
        title: 'Addons',
 | 
			
		||||
        component: Addons,
 | 
			
		||||
        component: AddonList,
 | 
			
		||||
        hidden: false,
 | 
			
		||||
        type: 'protected',
 | 
			
		||||
        layout: 'main',
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { IAddons } from '../../../../interfaces/addons';
 | 
			
		||||
import { IAddon } from '../../../../interfaces/addons';
 | 
			
		||||
import useAPI from '../useApi/useApi';
 | 
			
		||||
 | 
			
		||||
const useAddonsApi = () => {
 | 
			
		||||
@ -8,7 +8,7 @@ const useAddonsApi = () => {
 | 
			
		||||
 | 
			
		||||
    const URI = 'api/admin/addons';
 | 
			
		||||
 | 
			
		||||
    const createAddon = async (addonConfig: IAddons) => {
 | 
			
		||||
    const createAddon = async (addonConfig: IAddon) => {
 | 
			
		||||
        const path = URI;
 | 
			
		||||
        const req = createRequest(path, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
@ -38,7 +38,7 @@ const useAddonsApi = () => {
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const updateAddon = async (addonConfig: IAddons) => {
 | 
			
		||||
    const updateAddon = async (addonConfig: IAddon) => {
 | 
			
		||||
        const path = `${URI}/${addonConfig.id}`;
 | 
			
		||||
        const req = createRequest(path, {
 | 
			
		||||
            method: 'PUT',
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,6 @@
 | 
			
		||||
export interface IAddons {
 | 
			
		||||
import { ITagType } from './tags';
 | 
			
		||||
 | 
			
		||||
export interface IAddon {
 | 
			
		||||
    id: number;
 | 
			
		||||
    provider: string;
 | 
			
		||||
    description: string;
 | 
			
		||||
@ -6,3 +8,32 @@ export interface IAddons {
 | 
			
		||||
    events: string[];
 | 
			
		||||
    parameters: object;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IAddonProvider {
 | 
			
		||||
    description: string;
 | 
			
		||||
    displayName: string;
 | 
			
		||||
    documentationUrl: string;
 | 
			
		||||
    events: string[];
 | 
			
		||||
    name: string;
 | 
			
		||||
    parameters: IAddonProviderParams[];
 | 
			
		||||
    tagTypes: ITagType[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IAddonProviderParams {
 | 
			
		||||
    name: string;
 | 
			
		||||
    displayName: string;
 | 
			
		||||
    type: string;
 | 
			
		||||
    required: boolean;
 | 
			
		||||
    sensitive: boolean;
 | 
			
		||||
    placeholder?: string;
 | 
			
		||||
    description?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IAddonConfig {
 | 
			
		||||
    description: string;
 | 
			
		||||
    enabled: boolean;
 | 
			
		||||
    events: string[];
 | 
			
		||||
    id: number;
 | 
			
		||||
    parameters: Record<string, any>;
 | 
			
		||||
    provider: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,14 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import AddonForm from '../../component/addons/form-addon-container';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
const render = ({ match: { params }, history }) => (
 | 
			
		||||
    <AddonForm provider={params.provider} title="Configure addon" history={history} />
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
render.propTypes = {
 | 
			
		||||
    match: PropTypes.object.isRequired,
 | 
			
		||||
    history: PropTypes.object.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default render;
 | 
			
		||||
@ -1,14 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import AddonForm from '../../component/addons/form-addon-container';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
const render = ({ match: { params }, history }) => (
 | 
			
		||||
    <AddonForm addonId={params.id} title="Edit addon" history={history} />
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
render.propTypes = {
 | 
			
		||||
    match: PropTypes.object.isRequired,
 | 
			
		||||
    history: PropTypes.object.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default render;
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import Addons from '../../component/addons';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
const render = ({ history }) => <Addons history={history} />;
 | 
			
		||||
 | 
			
		||||
render.propTypes = {
 | 
			
		||||
    history: PropTypes.object.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default render;
 | 
			
		||||
@ -1,69 +0,0 @@
 | 
			
		||||
export const addonSimple = {
 | 
			
		||||
    addons: [],
 | 
			
		||||
    providers: [
 | 
			
		||||
        {
 | 
			
		||||
            name: 'webhook',
 | 
			
		||||
            displayName: 'Webhook',
 | 
			
		||||
            parameters: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'url',
 | 
			
		||||
                    displayName: 'Webhook URL',
 | 
			
		||||
                    type: 'string',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'unleashUrl',
 | 
			
		||||
                    displayName: 'Unleash Admin UI url',
 | 
			
		||||
                    type: 'text',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'bodyTemplate',
 | 
			
		||||
                    displayName: 'Body template',
 | 
			
		||||
                    description: 'You may format the body using a mustache template.',
 | 
			
		||||
                    type: 'text',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            events: ['feature-created', 'feature-updated', 'feature-archived', 'feature-revived'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const addonConfig = {
 | 
			
		||||
    id: 1,
 | 
			
		||||
    provider: 'webhook',
 | 
			
		||||
    enabled: true,
 | 
			
		||||
    description: null,
 | 
			
		||||
    parameters: {
 | 
			
		||||
        url: 'http://localhost:4242/webhook',
 | 
			
		||||
        bodyTemplate: "{'name': '{{event.data.name}}' }",
 | 
			
		||||
    },
 | 
			
		||||
    events: ['feature-updated', 'feature-created'],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const addonsWithConfig = {
 | 
			
		||||
    addons: [addonConfig],
 | 
			
		||||
    providers: [
 | 
			
		||||
        {
 | 
			
		||||
            name: 'webhook',
 | 
			
		||||
            displayName: 'Webhook',
 | 
			
		||||
            parameters: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'url',
 | 
			
		||||
                    displayName: 'Webhook URL',
 | 
			
		||||
                    type: 'string',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'unleashUrl',
 | 
			
		||||
                    displayName: 'Unleash Admin UI url',
 | 
			
		||||
                    type: 'text',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'bodyTemplate',
 | 
			
		||||
                    displayName: 'Body template',
 | 
			
		||||
                    description: 'You may format the body using a mustache template.',
 | 
			
		||||
                    type: 'text',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            events: ['feature-created', 'feature-updated', 'feature-archived', 'feature-revived'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
@ -1,189 +0,0 @@
 | 
			
		||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
 | 
			
		||||
 | 
			
		||||
exports[`should add addon-config 1`] = `
 | 
			
		||||
Object {
 | 
			
		||||
  "addons": Array [
 | 
			
		||||
    Object {
 | 
			
		||||
      "description": null,
 | 
			
		||||
      "enabled": true,
 | 
			
		||||
      "events": Array [
 | 
			
		||||
        "feature-updated",
 | 
			
		||||
        "feature-created",
 | 
			
		||||
      ],
 | 
			
		||||
      "id": 1,
 | 
			
		||||
      "parameters": Object {
 | 
			
		||||
        "bodyTemplate": "{'name': '{{event.data.name}}' }",
 | 
			
		||||
        "url": "http://localhost:4242/webhook",
 | 
			
		||||
      },
 | 
			
		||||
      "provider": "webhook",
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  "providers": Array [
 | 
			
		||||
    Object {
 | 
			
		||||
      "displayName": "Webhook",
 | 
			
		||||
      "events": Array [
 | 
			
		||||
        "feature-created",
 | 
			
		||||
        "feature-updated",
 | 
			
		||||
        "feature-archived",
 | 
			
		||||
        "feature-revived",
 | 
			
		||||
      ],
 | 
			
		||||
      "name": "webhook",
 | 
			
		||||
      "parameters": Array [
 | 
			
		||||
        Object {
 | 
			
		||||
          "displayName": "Webhook URL",
 | 
			
		||||
          "name": "url",
 | 
			
		||||
          "type": "string",
 | 
			
		||||
        },
 | 
			
		||||
        Object {
 | 
			
		||||
          "displayName": "Unleash Admin UI url",
 | 
			
		||||
          "name": "unleashUrl",
 | 
			
		||||
          "type": "text",
 | 
			
		||||
        },
 | 
			
		||||
        Object {
 | 
			
		||||
          "description": "You may format the body using a mustache template.",
 | 
			
		||||
          "displayName": "Body template",
 | 
			
		||||
          "name": "bodyTemplate",
 | 
			
		||||
          "type": "text",
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`should be default state 1`] = `
 | 
			
		||||
Object {
 | 
			
		||||
  "addons": Array [],
 | 
			
		||||
  "providers": Array [],
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`should be merged state all 1`] = `
 | 
			
		||||
Object {
 | 
			
		||||
  "addons": Array [],
 | 
			
		||||
  "providers": Array [
 | 
			
		||||
    Object {
 | 
			
		||||
      "displayName": "Webhook",
 | 
			
		||||
      "events": Array [
 | 
			
		||||
        "feature-created",
 | 
			
		||||
        "feature-updated",
 | 
			
		||||
        "feature-archived",
 | 
			
		||||
        "feature-revived",
 | 
			
		||||
      ],
 | 
			
		||||
      "name": "webhook",
 | 
			
		||||
      "parameters": Array [
 | 
			
		||||
        Object {
 | 
			
		||||
          "displayName": "Webhook URL",
 | 
			
		||||
          "name": "url",
 | 
			
		||||
          "type": "string",
 | 
			
		||||
        },
 | 
			
		||||
        Object {
 | 
			
		||||
          "displayName": "Unleash Admin UI url",
 | 
			
		||||
          "name": "unleashUrl",
 | 
			
		||||
          "type": "text",
 | 
			
		||||
        },
 | 
			
		||||
        Object {
 | 
			
		||||
          "description": "You may format the body using a mustache template.",
 | 
			
		||||
          "displayName": "Body template",
 | 
			
		||||
          "name": "bodyTemplate",
 | 
			
		||||
          "type": "text",
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`should clear addon-config on logout 1`] = `
 | 
			
		||||
Object {
 | 
			
		||||
  "addons": Array [],
 | 
			
		||||
  "providers": Array [],
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`should remove addon-config 1`] = `
 | 
			
		||||
Object {
 | 
			
		||||
  "addons": Array [],
 | 
			
		||||
  "providers": Array [
 | 
			
		||||
    Object {
 | 
			
		||||
      "displayName": "Webhook",
 | 
			
		||||
      "events": Array [
 | 
			
		||||
        "feature-created",
 | 
			
		||||
        "feature-updated",
 | 
			
		||||
        "feature-archived",
 | 
			
		||||
        "feature-revived",
 | 
			
		||||
      ],
 | 
			
		||||
      "name": "webhook",
 | 
			
		||||
      "parameters": Array [
 | 
			
		||||
        Object {
 | 
			
		||||
          "displayName": "Webhook URL",
 | 
			
		||||
          "name": "url",
 | 
			
		||||
          "type": "string",
 | 
			
		||||
        },
 | 
			
		||||
        Object {
 | 
			
		||||
          "displayName": "Unleash Admin UI url",
 | 
			
		||||
          "name": "unleashUrl",
 | 
			
		||||
          "type": "text",
 | 
			
		||||
        },
 | 
			
		||||
        Object {
 | 
			
		||||
          "description": "You may format the body using a mustache template.",
 | 
			
		||||
          "displayName": "Body template",
 | 
			
		||||
          "name": "bodyTemplate",
 | 
			
		||||
          "type": "text",
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`should update addon-config 1`] = `
 | 
			
		||||
Object {
 | 
			
		||||
  "addons": Array [
 | 
			
		||||
    Object {
 | 
			
		||||
      "description": "new desc",
 | 
			
		||||
      "enabled": false,
 | 
			
		||||
      "events": Array [
 | 
			
		||||
        "feature-updated",
 | 
			
		||||
        "feature-created",
 | 
			
		||||
      ],
 | 
			
		||||
      "id": 1,
 | 
			
		||||
      "parameters": Object {
 | 
			
		||||
        "bodyTemplate": "{'name': '{{event.data.name}}' }",
 | 
			
		||||
        "url": "http://localhost:4242/webhook",
 | 
			
		||||
      },
 | 
			
		||||
      "provider": "webhook",
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  "providers": Array [
 | 
			
		||||
    Object {
 | 
			
		||||
      "displayName": "Webhook",
 | 
			
		||||
      "events": Array [
 | 
			
		||||
        "feature-created",
 | 
			
		||||
        "feature-updated",
 | 
			
		||||
        "feature-archived",
 | 
			
		||||
        "feature-revived",
 | 
			
		||||
      ],
 | 
			
		||||
      "name": "webhook",
 | 
			
		||||
      "parameters": Array [
 | 
			
		||||
        Object {
 | 
			
		||||
          "displayName": "Webhook URL",
 | 
			
		||||
          "name": "url",
 | 
			
		||||
          "type": "string",
 | 
			
		||||
        },
 | 
			
		||||
        Object {
 | 
			
		||||
          "displayName": "Unleash Admin UI url",
 | 
			
		||||
          "name": "unleashUrl",
 | 
			
		||||
          "type": "text",
 | 
			
		||||
        },
 | 
			
		||||
        Object {
 | 
			
		||||
          "description": "You may format the body using a mustache template.",
 | 
			
		||||
          "displayName": "Body template",
 | 
			
		||||
          "name": "bodyTemplate",
 | 
			
		||||
          "type": "text",
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
@ -1,113 +0,0 @@
 | 
			
		||||
import configureMockStore from 'redux-mock-store';
 | 
			
		||||
import thunk from 'redux-thunk';
 | 
			
		||||
import fetchMock from 'fetch-mock';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    RECEIVE_ADDON_CONFIG,
 | 
			
		||||
    ERROR_RECEIVE_ADDON_CONFIG,
 | 
			
		||||
    REMOVE_ADDON_CONFIG,
 | 
			
		||||
    UPDATE_ADDON_CONFIG,
 | 
			
		||||
    ADD_ADDON_CONFIG,
 | 
			
		||||
    fetchAddons,
 | 
			
		||||
    removeAddon,
 | 
			
		||||
    updateAddon,
 | 
			
		||||
    createAddon,
 | 
			
		||||
} from '../actions';
 | 
			
		||||
 | 
			
		||||
const middlewares = [thunk];
 | 
			
		||||
const mockStore = configureMockStore(middlewares);
 | 
			
		||||
 | 
			
		||||
afterEach(() => {
 | 
			
		||||
    fetchMock.restore();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('creates RECEIVE_ADDON_CONFIG when fetching addons has been done', () => {
 | 
			
		||||
    fetchMock.getOnce('api/admin/addons', {
 | 
			
		||||
        body: { addons: { providers: [{ name: 'webhook' }] } },
 | 
			
		||||
        headers: { 'content-type': 'application/json' },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const expectedActions = [{ type: RECEIVE_ADDON_CONFIG, value: { addons: { providers: [{ name: 'webhook' }] } } }];
 | 
			
		||||
    const store = mockStore({ addons: [] });
 | 
			
		||||
 | 
			
		||||
    return store.dispatch(fetchAddons()).then(() => {
 | 
			
		||||
        // return of async actions
 | 
			
		||||
        expect(store.getActions()).toEqual(expectedActions);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('creates RECEIVE_ADDON_CONFIG_ when fetching addons has been done', () => {
 | 
			
		||||
    fetchMock.getOnce('api/admin/addons', {
 | 
			
		||||
        body: { message: 'Server error' },
 | 
			
		||||
        headers: { 'content-type': 'application/json' },
 | 
			
		||||
        status: 500,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const store = mockStore({ addons: [] });
 | 
			
		||||
 | 
			
		||||
    return store.dispatch(fetchAddons()).catch(e => {
 | 
			
		||||
        // return of async actions
 | 
			
		||||
        expect(store.getActions()[0].type).toEqual(ERROR_RECEIVE_ADDON_CONFIG);
 | 
			
		||||
        expect(e.message).toEqual('Unexpected exception when talking to unleash-api');
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('creates REMOVE_ADDON_CONFIG when delete addon has been done', () => {
 | 
			
		||||
    const addon = {
 | 
			
		||||
        id: 1,
 | 
			
		||||
        provider: 'webhook',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    fetchMock.deleteOnce('api/admin/addons/1', {
 | 
			
		||||
        status: 200,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const expectedActions = [{ type: REMOVE_ADDON_CONFIG, value: addon }];
 | 
			
		||||
    const store = mockStore({ addons: [] });
 | 
			
		||||
 | 
			
		||||
    return store.dispatch(removeAddon(addon)).then(() => {
 | 
			
		||||
        // return of async actions
 | 
			
		||||
        expect(store.getActions()).toEqual(expectedActions);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('creates UPDATE_ADDON_CONFIG when delete addon has been done', () => {
 | 
			
		||||
    const addon = {
 | 
			
		||||
        id: 1,
 | 
			
		||||
        provider: 'webhook',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    fetchMock.putOnce('api/admin/addons/1', {
 | 
			
		||||
        headers: { 'content-type': 'application/json' },
 | 
			
		||||
        status: 200,
 | 
			
		||||
        body: addon,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const expectedActions = [{ type: UPDATE_ADDON_CONFIG, value: addon }];
 | 
			
		||||
    const store = mockStore({ addons: [] });
 | 
			
		||||
 | 
			
		||||
    return store.dispatch(updateAddon(addon)).then(() => {
 | 
			
		||||
        // return of async actions
 | 
			
		||||
        expect(store.getActions()).toEqual(expectedActions);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('creates ADD_ADDON_CONFIG when delete addon has been done', () => {
 | 
			
		||||
    const addon = {
 | 
			
		||||
        provider: 'webhook',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    fetchMock.postOnce('api/admin/addons', {
 | 
			
		||||
        headers: { 'content-type': 'application/json' },
 | 
			
		||||
        status: 200,
 | 
			
		||||
        body: addon,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const expectedActions = [{ type: ADD_ADDON_CONFIG, value: addon }];
 | 
			
		||||
    const store = mockStore({ addons: [] });
 | 
			
		||||
 | 
			
		||||
    return store.dispatch(createAddon(addon)).then(() => {
 | 
			
		||||
        // return of async actions
 | 
			
		||||
        expect(store.getActions()).toEqual(expectedActions);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@ -1,54 +0,0 @@
 | 
			
		||||
import reducer from '../index';
 | 
			
		||||
import { RECEIVE_ADDON_CONFIG, ADD_ADDON_CONFIG, REMOVE_ADDON_CONFIG, UPDATE_ADDON_CONFIG } from '../actions';
 | 
			
		||||
import { addonSimple, addonsWithConfig, addonConfig } from '../__testdata__/data';
 | 
			
		||||
import { USER_LOGOUT } from '../../user/actions';
 | 
			
		||||
 | 
			
		||||
test('should be default state', () => {
 | 
			
		||||
    const state = reducer(undefined, {});
 | 
			
		||||
    expect(state.toJS()).toMatchSnapshot();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should be merged state all', () => {
 | 
			
		||||
    const state = reducer(undefined, { type: RECEIVE_ADDON_CONFIG, value: addonSimple });
 | 
			
		||||
    expect(state.toJS()).toMatchSnapshot();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should add addon-config', () => {
 | 
			
		||||
    let state = reducer(undefined, { type: RECEIVE_ADDON_CONFIG, value: addonSimple });
 | 
			
		||||
    state = reducer(state, { type: ADD_ADDON_CONFIG, value: addonConfig });
 | 
			
		||||
 | 
			
		||||
    const data = state.toJS();
 | 
			
		||||
    expect(data).toMatchSnapshot();
 | 
			
		||||
    expect(data.addons.length).toBe(1);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should remove addon-config', () => {
 | 
			
		||||
    let state = reducer(undefined, { type: RECEIVE_ADDON_CONFIG, value: addonsWithConfig });
 | 
			
		||||
    state = reducer(state, { type: REMOVE_ADDON_CONFIG, value: addonConfig });
 | 
			
		||||
 | 
			
		||||
    const data = state.toJS();
 | 
			
		||||
    expect(data).toMatchSnapshot();
 | 
			
		||||
    expect(data.addons.length).toBe(0);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should update addon-config', () => {
 | 
			
		||||
    const updateAdddonConfig = { ...addonConfig, description: 'new desc', enabled: false };
 | 
			
		||||
 | 
			
		||||
    let state = reducer(undefined, { type: RECEIVE_ADDON_CONFIG, value: addonsWithConfig });
 | 
			
		||||
    state = reducer(state, { type: UPDATE_ADDON_CONFIG, value: updateAdddonConfig });
 | 
			
		||||
 | 
			
		||||
    const data = state.toJS();
 | 
			
		||||
    expect(data).toMatchSnapshot();
 | 
			
		||||
    expect(data.addons.length).toBe(1);
 | 
			
		||||
    expect(data.addons[0].description).toBe('new desc');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should clear addon-config on logout', () => {
 | 
			
		||||
    let state = reducer(undefined, { type: RECEIVE_ADDON_CONFIG, value: addonsWithConfig });
 | 
			
		||||
    state = reducer(state, { type: USER_LOGOUT });
 | 
			
		||||
 | 
			
		||||
    const data = state.toJS();
 | 
			
		||||
    expect(data).toMatchSnapshot();
 | 
			
		||||
    expect(data.addons.length).toBe(0);
 | 
			
		||||
    expect(data.providers.length).toBe(0);
 | 
			
		||||
});
 | 
			
		||||
@ -1,51 +0,0 @@
 | 
			
		||||
import api from './api';
 | 
			
		||||
import { dispatchError } from '../util';
 | 
			
		||||
 | 
			
		||||
export const RECEIVE_ADDON_CONFIG = 'RECEIVE_ADDON_CONFIG';
 | 
			
		||||
export const ERROR_RECEIVE_ADDON_CONFIG = 'ERROR_RECEIVE_ADDON_CONFIG';
 | 
			
		||||
export const REMOVE_ADDON_CONFIG = 'REMOVE_ADDON_CONFIG';
 | 
			
		||||
export const ERROR_REMOVING_ADDON_CONFIG = 'ERROR_REMOVING_ADDON_CONFIG';
 | 
			
		||||
export const ADD_ADDON_CONFIG = 'ADD_ADDON_CONFIG';
 | 
			
		||||
export const ERROR_ADD_ADDON_CONFIG = 'ERROR_ADD_ADDON_CONFIG';
 | 
			
		||||
export const UPDATE_ADDON_CONFIG = 'UPDATE_ADDON_CONFIG';
 | 
			
		||||
export const ERROR_UPDATE_ADDON_CONFIG = 'ERROR_UPDATE_ADDON_CONFIG';
 | 
			
		||||
 | 
			
		||||
// const receiveAddonConfig = value => ({ type: RECEIVE_ADDON_CONFIG, value });
 | 
			
		||||
const addAddonConfig = value => ({ type: ADD_ADDON_CONFIG, value });
 | 
			
		||||
const updateAdddonConfig = value => ({ type: UPDATE_ADDON_CONFIG, value });
 | 
			
		||||
const removeAddonconfig = value => ({ type: REMOVE_ADDON_CONFIG, value });
 | 
			
		||||
 | 
			
		||||
const success = (dispatch, type) => value => dispatch({ type, value });
 | 
			
		||||
 | 
			
		||||
export function fetchAddons() {
 | 
			
		||||
    return dispatch =>
 | 
			
		||||
        api
 | 
			
		||||
            .fetchAll()
 | 
			
		||||
            .then(success(dispatch, RECEIVE_ADDON_CONFIG))
 | 
			
		||||
            .catch(dispatchError(dispatch, ERROR_RECEIVE_ADDON_CONFIG));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function removeAddon(addon) {
 | 
			
		||||
    return dispatch =>
 | 
			
		||||
        api
 | 
			
		||||
            .remove(addon)
 | 
			
		||||
            .then(() => dispatch(removeAddonconfig(addon)))
 | 
			
		||||
            .catch(dispatchError(dispatch, ERROR_REMOVING_ADDON_CONFIG));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createAddon(addon) {
 | 
			
		||||
    return dispatch =>
 | 
			
		||||
        api
 | 
			
		||||
            .create(addon)
 | 
			
		||||
            .then(res => res.json())
 | 
			
		||||
            .then(value => dispatch(addAddonConfig(value)))
 | 
			
		||||
            .catch(dispatchError(dispatch, ERROR_ADD_ADDON_CONFIG));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function updateAddon(addon) {
 | 
			
		||||
    return dispatch =>
 | 
			
		||||
        api
 | 
			
		||||
            .update(addon)
 | 
			
		||||
            .then(() => dispatch(updateAdddonConfig(addon)))
 | 
			
		||||
            .catch(dispatchError(dispatch, ERROR_UPDATE_ADDON_CONFIG));
 | 
			
		||||
}
 | 
			
		||||
@ -1,44 +0,0 @@
 | 
			
		||||
import { formatApiPath } from '../../utils/format-path';
 | 
			
		||||
import { throwIfNotSuccess, headers } from '../api-helper';
 | 
			
		||||
 | 
			
		||||
const URI = formatApiPath(`api/admin/addons`);
 | 
			
		||||
 | 
			
		||||
function fetchAll() {
 | 
			
		||||
    return fetch(URI, { credentials: 'include' })
 | 
			
		||||
        .then(throwIfNotSuccess)
 | 
			
		||||
        .then(response => response.json());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function create(addonConfig) {
 | 
			
		||||
    return fetch(URI, {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        headers,
 | 
			
		||||
        body: JSON.stringify(addonConfig),
 | 
			
		||||
        credentials: 'include',
 | 
			
		||||
    }).then(throwIfNotSuccess);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function update(addonConfig) {
 | 
			
		||||
    return fetch(`${URI}/${addonConfig.id}`, {
 | 
			
		||||
        method: 'PUT',
 | 
			
		||||
        headers,
 | 
			
		||||
        body: JSON.stringify(addonConfig),
 | 
			
		||||
        credentials: 'include',
 | 
			
		||||
    }).then(throwIfNotSuccess);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function remove(addonConfig) {
 | 
			
		||||
    return fetch(`${URI}/${addonConfig.id}`, {
 | 
			
		||||
        method: 'DELETE',
 | 
			
		||||
        headers,
 | 
			
		||||
        credentials: 'include',
 | 
			
		||||
    }).then(throwIfNotSuccess);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const api = {
 | 
			
		||||
    fetchAll,
 | 
			
		||||
    create,
 | 
			
		||||
    update,
 | 
			
		||||
    remove,
 | 
			
		||||
};
 | 
			
		||||
export default api;
 | 
			
		||||
@ -1,33 +0,0 @@
 | 
			
		||||
import { Map as $Map, List, fromJS } from 'immutable';
 | 
			
		||||
import { RECEIVE_ADDON_CONFIG, ADD_ADDON_CONFIG, REMOVE_ADDON_CONFIG, UPDATE_ADDON_CONFIG } from './actions';
 | 
			
		||||
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
 | 
			
		||||
 | 
			
		||||
function getInitState() {
 | 
			
		||||
    return new $Map({
 | 
			
		||||
        providers: new List(),
 | 
			
		||||
        addons: new List(),
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const strategies = (state = getInitState(), action) => {
 | 
			
		||||
    switch (action.type) {
 | 
			
		||||
        case RECEIVE_ADDON_CONFIG:
 | 
			
		||||
            return fromJS(action.value);
 | 
			
		||||
        case ADD_ADDON_CONFIG: {
 | 
			
		||||
            return state.update('addons', arr => arr.push(fromJS(action.value)));
 | 
			
		||||
        }
 | 
			
		||||
        case REMOVE_ADDON_CONFIG:
 | 
			
		||||
            return state.update('addons', arr => arr.filter(a => a.get('id') !== action.value.id));
 | 
			
		||||
        case UPDATE_ADDON_CONFIG: {
 | 
			
		||||
            const index = state.get('addons').findIndex(item => item.get('id') === action.value.id);
 | 
			
		||||
            return state.setIn(['addons', index], fromJS(action.value));
 | 
			
		||||
        }
 | 
			
		||||
        case USER_LOGOUT:
 | 
			
		||||
        case USER_LOGIN:
 | 
			
		||||
            return getInitState();
 | 
			
		||||
        default:
 | 
			
		||||
            return state;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default strategies;
 | 
			
		||||
@ -22,12 +22,6 @@ import {
 | 
			
		||||
    ERROR_UPDATE_PROJECT,
 | 
			
		||||
} from '../project/actions';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    ERROR_ADD_ADDON_CONFIG,
 | 
			
		||||
    ERROR_UPDATE_ADDON_CONFIG,
 | 
			
		||||
    ERROR_REMOVING_ADDON_CONFIG,
 | 
			
		||||
} from '../addons/actions';
 | 
			
		||||
 | 
			
		||||
import { UPDATE_APPLICATION_FIELD } from '../application/actions';
 | 
			
		||||
 | 
			
		||||
import { FORBIDDEN } from '../util';
 | 
			
		||||
@ -59,9 +53,6 @@ const strategies = (state = getInitState(), action) => {
 | 
			
		||||
        case ERROR_RECEIVE_STRATEGIES:
 | 
			
		||||
        case ERROR_REMOVING_PROJECT:
 | 
			
		||||
        case ERROR_UPDATE_PROJECT:
 | 
			
		||||
        case ERROR_ADD_ADDON_CONFIG:
 | 
			
		||||
        case ERROR_UPDATE_ADDON_CONFIG:
 | 
			
		||||
        case ERROR_REMOVING_ADDON_CONFIG:
 | 
			
		||||
        case ERROR_ADD_PROJECT:
 | 
			
		||||
            return addErrorIfNotAlreadyInList(state, action.error.message);
 | 
			
		||||
        case FORBIDDEN:
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ import error from './error';
 | 
			
		||||
import user from './user';
 | 
			
		||||
import applications from './application';
 | 
			
		||||
import projects from './project';
 | 
			
		||||
import addons from './addons';
 | 
			
		||||
import apiCalls from './api-calls';
 | 
			
		||||
 | 
			
		||||
const unleashStore = combineReducers({
 | 
			
		||||
@ -15,7 +14,6 @@ const unleashStore = combineReducers({
 | 
			
		||||
    user,
 | 
			
		||||
    applications,
 | 
			
		||||
    projects,
 | 
			
		||||
    addons,
 | 
			
		||||
    apiCalls,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2077,6 +2077,18 @@
 | 
			
		||||
  resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
 | 
			
		||||
  integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
 | 
			
		||||
 | 
			
		||||
"@types/lodash.clonedeep@^4.5.6":
 | 
			
		||||
  version "4.5.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.6.tgz#3b6c40a0affe0799a2ce823b440a6cf33571d32b"
 | 
			
		||||
  integrity sha512-cE1jYr2dEg1wBImvXlNtp0xDoS79rfEdGozQVgliDZj1uERH4k+rmEMTudP9b4VQ8O6nRb5gPqft0QzEQGMQgA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/lodash" "*"
 | 
			
		||||
 | 
			
		||||
"@types/lodash@*":
 | 
			
		||||
  version "4.14.178"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8"
 | 
			
		||||
  integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==
 | 
			
		||||
 | 
			
		||||
"@types/minimatch@*":
 | 
			
		||||
  version "3.0.4"
 | 
			
		||||
  resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz"
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user