mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +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,50 +108,48 @@ 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'
|
||||
}
|
||||
onClick={() => toggleAddon(addon)}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={addon.enabled}
|
||||
show={<Visibility />}
|
||||
elseShow={<VisibilityOff />}
|
||||
/>
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(DELETE_ADDON)}
|
||||
show={
|
||||
<IconButton
|
||||
size="small"
|
||||
title="Remove addon"
|
||||
onClick={onRemoveAddon(addon)}
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_ADDON}
|
||||
tooltip={addon.enabled ? 'Disable addon' : 'Enable addon'}
|
||||
onClick={() => toggleAddon(addon)}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={addon.enabled}
|
||||
show={<Visibility />}
|
||||
elseShow={<VisibilityOff />}
|
||||
/>
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
permission={DELETE_ADDON}
|
||||
tooltip={'Remove Addon'}
|
||||
onClick={() => {
|
||||
setDeletedAddon(addon);
|
||||
setShowDelete(true);
|
||||
}}
|
||||
>
|
||||
<Delete />
|
||||
</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