1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-08 01:15:49 +02:00

Merge branch 'main' into refactor/applications

This commit is contained in:
Youssef Khedher 2022-02-10 11:14:10 +01:00 committed by GitHub
commit 5a05bb44a6
147 changed files with 1168 additions and 3370 deletions

View File

@ -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",

View File

@ -23,7 +23,6 @@ import ToastRenderer from './common/ToastRenderer/ToastRenderer';
interface IAppProps extends RouteComponentProps {
user: IAuthStatus;
fetchUiBootstrap: any;
feedback: any;
}
const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
// because we need the userId when the component load.
@ -135,7 +134,6 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
<Redirect to="/404" />
</Switch>
<Feedback
feedbackId="pnps"
openUrl="http://feedback.unleash.run"
/>
</LayoutPicker>
@ -147,10 +145,10 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
</SWRProvider>
);
};
// Set state to any for now, to avoid typing up entire state object while converting to tsx.
const mapStateToProps = (state: any) => ({
user: state.user.toJS(),
feedback: state.feedback,
});
export default connect(mapStateToProps)(App);

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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;

View File

@ -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>
);
};

View File

@ -1,3 +0,0 @@
import AvailableAddons from './AvailableAddons';
export default AvailableAddons;

View File

@ -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;

View File

@ -1,3 +0,0 @@
import ConfiguredAddons from './ConfiguredAddons';
export default ConfiguredAddons;

View File

@ -1,3 +0,0 @@
import AddonListComponent from './AddonList';
export default AddonListComponent;

View 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}
/>
);
};

View 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}
/>
);
};

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -12,7 +12,6 @@ import { useState } from 'react';
import { scrollToTop } from '../../../common/util';
const CreateApiToken = () => {
/* @ts-ignore */
const { setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const history = useHistory();

View File

@ -9,7 +9,6 @@ import PermissionButton from '../../../common/PermissionButton/PermissionButton'
import { ADMIN } from '../../../providers/AccessProvider/permissions';
const CreateProjectRole = () => {
/* @ts-ignore */
const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const history = useHistory();

View File

@ -12,7 +12,6 @@ import PermissionButton from '../../../common/PermissionButton/PermissionButton'
import { ADMIN } from '../../../providers/AccessProvider/permissions';
const CreateUser = () => {
/* @ts-ignore */
const { setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const history = useHistory();

View File

@ -38,7 +38,6 @@ export const useStyles = makeStyles(theme => ({
position: 'relative',
},
errorMessage: {
//@ts-ignore
fontSize: theme.fontSizes.smallBody,
color: theme.palette.error.main,
position: 'absolute',

View File

@ -4,7 +4,7 @@ export const useStyles = makeStyles(theme => ({
feedback: {
borderRadius: '12.5px',
backgroundColor: '#fff',
zIndex: '9999',
zIndex: 9999,
boxShadow: '2px 2px 4px 4px rgba(143,143,143, 0.25)',
padding: '1.5rem',
maxWidth: '400px',

View File

@ -9,24 +9,21 @@ import { useStyles } from './Feedback.styles';
import AnimateOnMount from '../AnimateOnMount/AnimateOnMount';
import ConditionallyRender from '../ConditionallyRender';
import { formatApiPath } from '../../../utils/format-path';
import { Action, Dispatch } from 'redux';
import UIContext from '../../../contexts/UIContext';
import useUser from '../../../hooks/api/getters/useUser/useUser';
import { PNPS_FEEDBACK_ID, showPnpsFeedback } from '../util';
interface IFeedbackProps {
show?: boolean;
hideFeedback: () => Dispatch<Action>;
fetchUser: () => void;
feedbackId: string;
openUrl: string;
}
const Feedback = ({ feedbackId, openUrl }: IFeedbackProps) => {
const Feedback = ({ openUrl }: IFeedbackProps) => {
const { showFeedback, setShowFeedback } = useContext(UIContext);
const { refetch, feedback } = useUser();
const [answeredNotNow, setAnsweredNotNow] = useState(false);
const styles = useStyles();
const commonStyles = useCommonStyles();
const feedbackId = PNPS_FEEDBACK_ID;
const onConfirm = async () => {
const url = formatApiPath('api/admin/feedback');
@ -41,7 +38,8 @@ const Feedback = ({ feedbackId, openUrl }: IFeedbackProps) => {
body: JSON.stringify({ feedbackId }),
});
await refetch();
} catch {
} catch (err) {
console.warn(err);
setShowFeedback(false);
}
@ -68,7 +66,8 @@ const Feedback = ({ feedbackId, openUrl }: IFeedbackProps) => {
body: JSON.stringify({ feedbackId, neverShow: true }),
});
await refetch();
} catch {
} catch (err) {
console.warn(err);
setShowFeedback(false);
}
@ -77,9 +76,7 @@ const Feedback = ({ feedbackId, openUrl }: IFeedbackProps) => {
}, 100);
};
const pnps = feedback.find(feedback => feedback.feedbackId === feedbackId);
if (pnps?.given || pnps?.neverShow) {
if (!showPnpsFeedback(feedback)) {
return null;
}

View File

@ -24,7 +24,6 @@ const FormTemplate: React.FC<ICreateProps> = ({
loading,
formatApiCode,
}) => {
// @ts-ignore-next-line
const { setToastData } = useToast();
const styles = useStyles();
const smallScreen = useMediaQuery(`(max-width:${900}px)`);

View File

@ -15,6 +15,7 @@ interface IPaginateUIProps {
prevPage: () => void;
setPageIndex: (idx: number) => void;
nextPage: () => void;
style?: React.CSSProperties;
}
const PaginateUI = ({

View File

@ -3,13 +3,19 @@ import { useContext } from 'react';
import AccessContext from '../../../contexts/AccessContext';
interface IPermissionIconButtonProps
extends React.HTMLProps<HTMLButtonElement> {
extends React.DetailedHTMLProps<
React.HTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> {
permission: string;
Icon?: React.ElementType;
tooltip: string;
onClick?: (e: any) => void;
projectId?: string;
environmentId?: string;
edge?: string;
className?: string;
title?: string;
}
const PermissionIconButton: React.FC<IPermissionIconButtonProps> = ({

View File

@ -3,16 +3,10 @@ import { Alert } from '@material-ui/lab';
import ConditionallyRender from '../ConditionallyRender';
import { Typography } from '@material-ui/core';
import { useStyles } from './Proclamation.styles';
import { IProclamationToast } from '../../../interfaces/uiConfig';
interface IProclamationProps {
toast?: IToast;
}
interface IToast {
message: string;
id: string;
severity: 'success' | 'info' | 'warning' | 'error';
link: string;
toast?: IProclamationToast;
}
const renderProclamation = (id: string) => {

View File

@ -29,7 +29,7 @@ export const useStyles = makeStyles(theme => ({
right: '0px',
bottom: '0px',
padding: '2rem 0',
zIndex: '500',
zIndex: 500,
position: 'fixed',
width: '100%',
height: '100%',

View File

@ -8,7 +8,7 @@ import ConditionallyRender from '../../ConditionallyRender';
import Close from '@material-ui/icons/Close';
const Toast = ({ title, text, type, confetti }: IToastData) => {
// @ts-ignore
// @ts-expect-error
const { setToast } = useContext(UIContext);
const styles = useStyles();

View File

@ -7,7 +7,7 @@ import AnimateOnMount from '../AnimateOnMount/AnimateOnMount';
import Toast from './Toast/Toast';
const ToastRenderer = () => {
// @ts-ignore-next-line
// @ts-expect-error
const { toastData, setToast } = useContext(UIContext);
const commonStyles = useCommonStyles();
const styles = useStyles();

View File

@ -118,12 +118,11 @@ export const modalStyles = {
export const updateIndexInArray = (array, index, newValue) =>
array.map((v, i) => (i === index ? newValue : v));
export const showPnpsFeedback = user => {
if (!user) return;
if (!user.feedback) return;
if (user.feedback.length > 0) {
const feedback = user.feedback.find(
feedback => feedback.feedbackId === 'pnps'
export const showPnpsFeedback = (feedbackList) => {
if (!feedbackList) return;
if (feedbackList.length > 0) {
const feedback = feedbackList.find(
feedback => feedback.feedbackId === PNPS_FEEDBACK_ID
);
if (!feedback) return false;
@ -143,3 +142,5 @@ export const showPnpsFeedback = user => {
}
return true;
};
export const PNPS_FEEDBACK_ID = 'pnps'

View File

@ -54,7 +54,6 @@ export const useStyles = makeStyles(theme => ({
position: 'relative',
},
errorMessage: {
//@ts-ignore
fontSize: theme.fontSizes.smallBody,
color: theme.palette.error.main,
position: 'absolute',

View File

@ -16,7 +16,6 @@ import { ADMIN } from '../../providers/AccessProvider/permissions';
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
const CreateEnvironment = () => {
/* @ts-ignore */
const { setToastApiError, setToastData } = useToast();
const { uiConfig } = useUiConfig();
const history = useHistory();
@ -76,7 +75,7 @@ const CreateEnvironment = () => {
<FormTemplate
loading={loading}
title="Create Environment"
description="Environments allow you to manage your
description="Environments allow you to manage your
product lifecycle from local development
through production. Your projects and
feature toggles are accessible in all your

View File

@ -38,7 +38,6 @@ export const useStyles = makeStyles(theme => ({
position: 'relative',
},
errorMessage: {
//@ts-ignore
fontSize: theme.fontSizes.smallBody,
color: theme.palette.error.main,
position: 'absolute',

View File

@ -6,7 +6,6 @@ export const useStyles = makeStyles(theme => ({
},
formHeader: {
fontWeight: 'bold',
//@ts-ignore
fontSize: theme.fontSizes.bodySize,
marginTop: '1.5rem',
marginBottom: '0.5rem',

View File

@ -12,7 +12,6 @@ import { useContext } from 'react';
import UIContext from '../../../contexts/UIContext';
const CreateFeature = () => {
/* @ts-ignore */
const { setToastData, setToastApiError } = useToast();
const { setShowFeedback } = useContext(UIContext);
const { uiConfig } = useUiConfig();

View File

@ -12,7 +12,6 @@ import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { UPDATE_FEATURE } from '../../providers/AccessProvider/permissions';
const EditFeature = () => {
/* @ts-ignore */
const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const history = useHistory();

View File

@ -38,7 +38,6 @@ export const useStyles = makeStyles(theme => ({
marginBottom: '0.5rem',
},
typeDescription: {
//@ts-ignore
fontSize: theme.fontSizes.smallBody,
color: theme.palette.grey[600],
top: '-13px',
@ -55,7 +54,6 @@ export const useStyles = makeStyles(theme => ({
position: 'relative',
},
errorMessage: {
//@ts-ignore
fontSize: theme.fontSizes.smallBody,
color: theme.palette.error.main,
position: 'absolute',

View File

@ -24,7 +24,7 @@ interface IFeatureToggleListNewProps {
projectId: string;
}
//@ts-ignore
// @ts-expect-error
const sortList = (list, sortOpt) => {
if (!list) {
return list;
@ -33,7 +33,7 @@ const sortList = (list, sortOpt) => {
return list;
}
if (sortOpt.type === 'string') {
//@ts-ignore
// @ts-expect-error
return list.sort((a, b) => {
const fieldA = a[sortOpt.field]?.toUpperCase();
const fieldB = b[sortOpt.field]?.toUpperCase();
@ -49,7 +49,7 @@ const sortList = (list, sortOpt) => {
});
}
if (sortOpt.type === 'date') {
//@ts-ignore
// @ts-expect-error
return list.sort((a, b) => {
const fieldA = new Date(a[sortOpt.field]);
const fieldB = new Date(b[sortOpt.field]);

View File

@ -75,7 +75,6 @@ const FeatureStatus = ({
<ConditionallyRender
condition={!!lastSeenAt}
show={
//@ts-ignore
<TimeAgo
date={lastSeenAt}
title=""

View File

@ -13,7 +13,7 @@ import { Info } from '@material-ui/icons';
import { weightTypes } from './enums';
import OverrideConfig from './OverrideConfig/OverrideConfig';
import { OverrideConfig } from './OverrideConfig/OverrideConfig';
import ConditionallyRender from '../../../../../common/ConditionallyRender';
import GeneralSelect from '../../../../../common/GeneralSelect/GeneralSelect';
import { useCommonStyles } from '../../../../../../common.styles';

View File

@ -1,6 +1,4 @@
import { connect } from 'react-redux';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { Grid, IconButton, TextField } from '@material-ui/core';
import { Delete } from '@material-ui/icons';
@ -10,17 +8,19 @@ import GeneralSelect from '../../../../../../common/GeneralSelect/GeneralSelect'
import { useCommonStyles } from '../../../../../../../common.styles';
import ConditionallyRender from '../../../../../../common/ConditionallyRender';
import InputListField from '../../../../../../common/input-list-field.jsx';
import useUnleashContext from '../../../../../../../hooks/api/getters/useUnleashContext/useUnleashContext';
const OverrideConfig = ({
export const OverrideConfig = ({
overrides,
updateOverrideType,
updateOverrideValues,
removeOverride,
contextDefinitions,
}) => {
const styles = useStyles();
const commonStyles = useCommonStyles();
const contextNames = contextDefinitions.map(c => ({
const { context } = useUnleashContext();
const contextNames = context.map(c => ({
key: c.name,
label: c.name,
}));
@ -34,9 +34,7 @@ const OverrideConfig = ({
};
return overrides.map((o, i) => {
const definition = contextDefinitions.find(
c => c.name === o.contextName
);
const definition = context.find(c => c.name === o.contextName);
const legalValues = definition ? definition.legalValues : [];
return (
@ -115,9 +113,3 @@ OverrideConfig.propTypes = {
updateOverrideValues: PropTypes.func.isRequired,
removeOverride: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
contextDefinitions: state.context.toJS(),
});
export default connect(mapStateToProps, {})(OverrideConfig);

View File

@ -147,7 +147,6 @@ const FeatureOverviewVariants = () => {
try {
const res = await patchFeatureVariants(projectId, featureId, patch);
// @ts-ignore
const { variants } = await res.json();
mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false);
setToastData({
@ -204,7 +203,6 @@ const FeatureOverviewVariants = () => {
if (patch.length === 0) return;
try {
const res = await patchFeatureVariants(projectId, featureId, patch);
// @ts-ignore
const { variants } = await res.json();
mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false);
setToastData({

View File

@ -1,6 +1,6 @@
import ConditionallyRender from '../../common/ConditionallyRender';
import { matchPath } from 'react-router';
import MainLayout from '../MainLayout';
import { MainLayout } from '../MainLayout/MainLayout';
const LayoutPicker = ({ children, location }) => {
const standalonePages = () => {

View File

@ -1,9 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import React, { ReactNode } from 'react';
import classnames from 'classnames';
import { makeStyles } from '@material-ui/styles';
import { makeStyles } from '@material-ui/core/styles';
import { Grid } from '@material-ui/core';
import styles from '../../styles.module.scss';
import ErrorContainer from '../../error/error-container';
import Header from '../../menu/Header/Header';
@ -11,6 +9,7 @@ import Footer from '../../menu/Footer/Footer';
import Proclamation from '../../common/Proclamation/Proclamation';
import BreadcrumbNav from '../../common/BreadcrumbNav/BreadcrumbNav';
import { ReactComponent as Texture } from '../../../assets/img/texture.svg';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
const useStyles = makeStyles(theme => ({
container: {
@ -27,18 +26,23 @@ const useStyles = makeStyles(theme => ({
},
}));
const MainLayout = ({ children, location, uiConfig }) => {
interface IMainLayoutProps {
children: ReactNode
}
export const MainLayout = ({ children }: IMainLayoutProps) => {
const muiStyles = useStyles();
const { uiConfig } = useUiConfig();
return (
<>
<Header location={location} />
<Header />
<Grid container className={muiStyles.container}>
<div className={classnames(styles.contentWrapper)}>
<Grid item className={styles.content} xs={12} sm={12}>
<div
className={muiStyles.contentContainer}
style={{ zIndex: '200' }}
style={{ zIndex: 200 }}
>
<BreadcrumbNav />
<Proclamation toast={uiConfig.toast} />
@ -52,7 +56,7 @@ const MainLayout = ({ children, location, uiConfig }) => {
position: 'fixed',
right: '0',
bottom: '-4px',
zIndex: '1',
zIndex: 1,
}}
>
<Texture />
@ -64,9 +68,3 @@ const MainLayout = ({ children, location, uiConfig }) => {
</>
);
};
MainLayout.propTypes = {
location: PropTypes.object.isRequired,
};
export default MainLayout;

View File

@ -1,10 +0,0 @@
import { connect } from 'react-redux';
import MainLayout from './MainLayout';
const mapStateToProps = (state, ownProps) => ({
uiConfig: state.uiConfig.toJS(),
location: ownProps.location,
children: ownProps.children,
});
export default connect(mapStateToProps)(MainLayout);

View File

@ -7,7 +7,7 @@ export const useStyles = makeStyles(theme => ({
padding: '0.5rem',
boxShadow: 'none',
position: 'relative',
zIndex: '300',
zIndex: 300,
},
links: {
display: 'flex',

View File

@ -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",
},

View File

@ -6,10 +6,8 @@ import Strategies from '../../page/strategies';
import HistoryPage from '../../page/history';
import HistoryTogglePage from '../../page/history/toggle';
import { ArchiveListContainer } from '../archive/ArchiveListContainer';
import ListTagTypes from '../../page/tag-types';
import Addons from '../../page/addons';
import AddonsCreate from '../../page/addons/create';
import AddonsEdit from '../../page/addons/edit';
import { TagTypeList } from '../tags/TagTypeList/TagTypeList';
import { AddonList } from '../addons/AddonList/AddonList';
import Admin from '../admin';
import AdminApi from '../admin/api';
import AdminInvoice from '../admin/invoice/InvoiceAdminPage';
@ -35,8 +33,8 @@ import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironme
import EditEnvironment from '../environments/EditEnvironment/EditEnvironment';
import CreateContext from '../context/CreateContext/CreateContext';
import EditContext from '../context/EditContext/EditContext';
import EditTagType from '../tagTypes/EditTagType/EditTagType';
import CreateTagType from '../tagTypes/CreateTagType/CreateTagType';
import EditTagType from '../tags/EditTagType/EditTagType';
import CreateTagType from '../tags/CreateTagType/CreateTagType';
import EditProject from '../project/Project/EditProject/EditProject';
import CreateProject from '../project/Project/CreateProject/CreateProject';
import CreateFeature from '../feature/CreateFeature/CreateFeature';
@ -45,6 +43,8 @@ import { ApplicationEdit } from '../application/ApplicationEdit/ApplicationEdit'
import { ApplicationList } from '../application/ApplicationList/ApplicationList';
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
@ -314,7 +314,7 @@ export const routes = [
{
path: '/tag-types',
title: 'Tag types',
component: ListTagTypes,
component: TagTypeList,
type: 'protected',
layout: 'main',
menu: { mobile: true, advanced: true },
@ -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',

View File

@ -10,7 +10,6 @@ import PermissionButton from '../../../common/PermissionButton/PermissionButton'
import { CREATE_PROJECT } from '../../../providers/AccessProvider/permissions';
const CreateProject = () => {
/* @ts-ignore */
const { setToastData, setToastApiError } = useToast();
const { refetch } = useUser();
const { uiConfig } = useUiConfig();

View File

@ -12,7 +12,7 @@ import useQueryParams from '../../../hooks/useQueryParams';
import { useEffect } from 'react';
import useTabs from '../../../hooks/useTabs';
import TabPanel from '../../common/TabNav/TabPanel';
import ProjectAccess from '../access-container';
import { ProjectAccess } from '../ProjectAccess/ProjectAccess';
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment';
import ProjectOverview from './ProjectOverview';
import ProjectHealth from './ProjectHealth/ProjectHealth';

View File

@ -38,7 +38,7 @@ export const useStyles = makeStyles(theme => ({
position: 'relative',
},
errorMessage: {
//@ts-ignore
// @ts-expect-error
fontSize: theme.fontSizes.smallBody,
color: theme.palette.error.main,
position: 'absolute',

View File

@ -11,18 +11,11 @@ export const useStyles = makeStyles(theme => ({
backgroundColor: '#efefef',
marginTop: '2rem',
},
actionList: {
display: 'flex',
alignItems: 'center',
},
inputLabel: { backgroundColor: '#fff' },
roleName: {
fontWeight: 'bold',
padding: '5px 0px',
},
iconButton: {
marginLeft: '0.5rem',
},
menuItem: {
width: '340px',
whiteSpace: 'normal',

View File

@ -1,71 +1,40 @@
/* eslint-disable react/jsx-no-target-blank */
import { useEffect, useState } from 'react';
import {
Avatar,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
List,
ListItem,
ListItemAvatar,
ListItemSecondaryAction,
ListItemText,
MenuItem,
} from '@material-ui/core';
import { Delete } from '@material-ui/icons';
import React, { useState } from 'react';
import { Alert } from '@material-ui/lab';
import AddUserComponent from '../access-add-user';
import { ProjectAccessAddUser } from './ProjectAccessAddUser/ProjectAccessAddUser';
import projectApi from '../../../store/project/api';
import PageContent from '../../common/PageContent';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import { useStyles } from './ProjectAccess.styles';
import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton';
import { useParams } from 'react-router-dom';
import { IFeatureViewParams } from '../../../interfaces/params';
import ProjectRoleSelect from './ProjectRoleSelect/ProjectRoleSelect';
import { IProjectViewParams } from '../../../interfaces/params';
import usePagination from '../../../hooks/usePagination';
import PaginateUI from '../../common/PaginateUI/PaginateUI';
import useToast from '../../../hooks/useToast';
import ConfirmDialogue from '../../common/Dialogue';
import useProjectAccess, {
IProjectAccessUser,
} from '../../../hooks/api/getters/useProjectAccess/useProjectAccess';
import useProjectApi from '../../../hooks/api/actions/useProjectApi/useProjectApi';
import HeaderTitle from '../../common/HeaderTitle';
import { ProjectAccessList } from './ProjectAccessList/ProjectAccessList';
const ProjectAccess = () => {
const { id } = useParams<IFeatureViewParams>();
export const ProjectAccess = () => {
const { id: projectId } = useParams<IProjectViewParams>();
const styles = useStyles();
const [roles, setRoles] = useState([]);
const [users, setUsers] = useState([]);
const [error, setError] = useState();
const { setToastData, setToastApiError } = useToast();
const { access, refetchProjectAccess } = useProjectAccess(projectId);
const { setToastData } = useToast();
const { isOss } = useUiConfig();
const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } =
usePagination(users, 10);
usePagination(access.users, 10);
const { removeUserFromRole, addUserToRole } = useProjectApi();
const [showDelDialogue, setShowDelDialogue] = useState(false);
const [user, setUser] = useState({});
useEffect(() => {
fetchAccess();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
const fetchAccess = async () => {
try {
const access = await projectApi.fetchAccess(id);
setRoles(access.roles);
setUsers(
access.users.map(u => ({ ...u, name: u.name || '(No name)' }))
);
} catch (e) {
setToastApiError(e.toString());
}
};
const [user, setUser] = useState<IProjectAccessUser | undefined>();
if (isOss()) {
return (
<PageContent>
<PageContent headerContent={<HeaderTitle title="Project Access" />}>
<Alert severity="error">
Controlling access to projects requires a paid version of
Unleash. Check out{' '}
@ -78,58 +47,49 @@ const ProjectAccess = () => {
);
}
const handleRoleChange = (userId, currRoleId) => async evt => {
const roleId = evt.target.value;
try {
await projectApi.removeUserFromRole(id, currRoleId, userId);
await projectApi.addUserToRole(id, roleId, userId).then(() => {
const handleRoleChange =
(userId: number, currRoleId: number) =>
async (
evt: React.ChangeEvent<{
name?: string;
value: unknown;
}>
) => {
const roleId = Number(evt.target.value);
try {
await removeUserFromRole(projectId, currRoleId, userId);
await addUserToRole(projectId, roleId, userId);
refetchProjectAccess();
setToastData({
type: 'success',
title: 'User role changed successfully',
});
});
const newUsers = users.map(u => {
if (u.id === userId) {
return { ...u, roleId };
} else return u;
});
setUsers(newUsers);
} catch (err) {
setToastData({
type: 'error',
title: err.message || 'Server problems when adding users.',
});
}
} catch (err: any) {
setToastData({
type: 'error',
title: err.message || 'Server problems when adding users.',
});
}
};
const handleRemoveAccess = (user: IProjectAccessUser) => {
setUser(user);
setShowDelDialogue(true);
};
const addUser = async (userId, roleId) => {
try {
await projectApi.addUserToRole(id, roleId, userId);
await fetchAccess().then(() => {
setToastData({
type: 'success',
title: 'Successfully added user to the project',
});
});
} catch (err) {
setToastData({
type: 'error',
title: err.message || 'Server problems when adding users.',
});
}
};
const removeAccess = (user: IProjectAccessUser | undefined) => async () => {
if (!user) return;
const { id, roleId } = user;
const removeAccess = (userId: number, roleId: number) => async () => {
try {
await projectApi.removeUserFromRole(id, roleId, userId).then(() => {
setToastData({
type: 'success',
title: 'User have been removed from project',
});
await removeUserFromRole(projectId, roleId, id);
refetchProjectAccess();
setToastData({
type: 'success',
title: 'The user has been removed from project',
});
const newUsers = users.filter(u => u.id !== userId);
setUsers(newUsers);
} catch (err) {
} catch (err: any) {
setToastData({
type: 'error',
title: err.message || 'Server problems when adding users.',
@ -138,91 +98,20 @@ const ProjectAccess = () => {
setShowDelDialogue(false);
};
const handleCloseError = () => {
setError(undefined);
};
return (
<PageContent className={styles.pageContent}>
<AddUserComponent roles={roles} addUserToRole={addUser} />
<Dialog
open={!!error}
onClose={handleCloseError}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{'Error'}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{error}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={handleCloseError}
color="secondary"
autoFocus
>
Close
</Button>
</DialogActions>
</Dialog>
<div className={styles.divider}></div>
<List>
{page.map(user => {
const labelId = `checkbox-list-secondary-label-${user.id}`;
return (
<ListItem key={user.id} button>
<ListItemAvatar>
<Avatar alt={user.name} src={user.imageUrl} />
</ListItemAvatar>
<ListItemText
id={labelId}
primary={user.name}
secondary={user.email || user.username}
/>
<ListItemSecondaryAction
className={styles.actionList}
>
<ProjectRoleSelect
labelId={`role-${user.id}-select-label`}
id={`role-${user.id}-select`}
key={user.id}
placeholder="Choose role"
onChange={handleRoleChange(
user.id,
user.roleId
)}
roles={roles}
value={user.roleId || ''}
>
<MenuItem value="" disabled>
Choose role
</MenuItem>
</ProjectRoleSelect>
<PageContent
headerContent={<HeaderTitle title="Project Roles"></HeaderTitle>}
className={styles.pageContent}
>
<ProjectAccessAddUser roles={access?.roles} />
<PermissionIconButton
className={styles.iconButton}
edge="end"
aria-label="delete"
title="Remove access"
onClick={() => {
setUser(user);
setShowDelDialogue(true);
}}
disabled={users.length === 1}
tooltip={
users.length === 1
? 'A project must have at least one owner'
: 'Remove access'
}
>
<Delete />
</PermissionIconButton>
</ListItemSecondaryAction>
</ListItem>
);
})}
<div className={styles.divider}></div>
<ProjectAccessList
handleRoleChange={handleRoleChange}
handleRemoveAccess={handleRemoveAccess}
page={page}
access={access}
>
<PaginateUI
pages={pages}
pageIndex={pageIndex}
@ -231,12 +120,13 @@ const ProjectAccess = () => {
prevPage={prevPage}
style={{ bottom: '-21px' }}
/>
</List>
</ProjectAccessList>
<ConfirmDialogue
open={showDelDialogue}
onClick={removeAccess(user.id, user.roleId)}
onClick={removeAccess(user)}
onClose={() => {
setUser({});
setUser(undefined);
setShowDelDialogue(false);
}}
title="Really remove user from this project"
@ -244,5 +134,3 @@ const ProjectAccess = () => {
</PageContent>
);
};
export default ProjectAccess;

View File

@ -0,0 +1,236 @@
import React, { ChangeEvent, useEffect, useState } from 'react';
import {
TextField,
CircularProgress,
Grid,
Button,
InputAdornment,
} from '@material-ui/core';
import { Search } from '@material-ui/icons';
import Autocomplete from '@material-ui/lab/Autocomplete';
import { Alert } from '@material-ui/lab';
import { ProjectRoleSelect } from '../ProjectRoleSelect/ProjectRoleSelect';
import useProjectApi from '../../../../hooks/api/actions/useProjectApi/useProjectApi';
import { useParams } from 'react-router-dom';
import useToast from '../../../../hooks/useToast';
import useProjectAccess, {
IProjectAccessUser,
} from '../../../../hooks/api/getters/useProjectAccess/useProjectAccess';
import { IProjectRole } from '../../../../interfaces/role';
import ConditionallyRender from '../../../common/ConditionallyRender';
interface IProjectAccessAddUserProps {
roles: IProjectRole[];
}
export const ProjectAccessAddUser = ({ roles }: IProjectAccessAddUserProps) => {
const { id } = useParams<{ id: string }>();
const [user, setUser] = useState<IProjectAccessUser | undefined>();
const [role, setRole] = useState<IProjectRole | undefined>();
const [options, setOptions] = useState([]);
const [loading, setLoading] = useState(false);
const { setToastData } = useToast();
const { refetchProjectAccess, access } = useProjectAccess(id);
const { searchProjectUser, addUserToRole } = useProjectApi();
useEffect(() => {
if (roles.length > 0) {
const regularRole = roles.find(
r => r.name.toLowerCase() === 'regular'
);
setRole(regularRole || roles[0]);
}
}, [roles]);
const search = async (query: string) => {
if (query.length > 1) {
setLoading(true);
const result = await searchProjectUser(query);
const userSearchResults = await result.json();
const filteredUsers = userSearchResults.filter(
(selectedUser: IProjectAccessUser) => {
const selected = access.users.find(
(user: IProjectAccessUser) =>
user.id === selectedUser.id
);
return !selected;
}
);
setOptions(filteredUsers);
} else {
setOptions([]);
}
setLoading(false);
};
const handleQueryUpdate = (evt: { target: { value: string } }) => {
const q = evt.target.value;
search(q);
};
const handleBlur = () => {
if (options.length > 0) {
const user = options[0];
setUser(user);
}
};
const handleSelectUser = (
evt: ChangeEvent<{}>,
selectedUser: string | IProjectAccessUser | null
) => {
setOptions([]);
if (typeof selectedUser === 'string' || selectedUser === null) {
return;
}
if (selectedUser?.id) {
setUser(selectedUser);
}
};
const handleRoleChange = (
evt: React.ChangeEvent<{
name?: string | undefined;
value: unknown;
}>
) => {
const roleId = Number(evt.target.value);
const role = roles.find(role => role.id === roleId);
if (role) {
setRole(role);
}
};
const handleSubmit = async (evt: React.SyntheticEvent) => {
evt.preventDefault();
if (!role || !user) {
setToastData({
type: 'error',
title: 'Invalid selection',
text: `The selected user or role does not exist`,
});
return;
}
try {
await addUserToRole(id, role.id, user.id);
refetchProjectAccess();
setUser(undefined);
setOptions([]);
setToastData({
type: 'success',
title: 'Added user to project',
text: `User added to the project with the role of ${role.name}`,
});
} catch (e: any) {
let error;
if (
e
.toString()
.includes(`User already has access to project=${id}`)
) {
error = `User already has access to project ${id}`;
} else {
error = e.toString() || 'Server problems when adding users.';
}
setToastData({
type: 'error',
title: error,
});
}
};
const getOptionLabel = (option: IProjectAccessUser) => {
if (option) {
return `${option.name || '(Empty name)'} <${
option.email || option.username
}>`;
} else return '';
};
return (
<>
<Alert severity="info" style={{ marginBottom: '20px' }}>
The user must have an Unleash root role before added to the
project.
</Alert>
<Grid container spacing={3} alignItems="flex-end">
<Grid item>
<Autocomplete
id="add-user-component"
style={{ width: 300 }}
noOptionsText="No users found."
onChange={handleSelectUser}
onBlur={() => handleBlur()}
value={user || ''}
freeSolo
getOptionSelected={() => true}
filterOptions={o => o}
getOptionLabel={getOptionLabel}
options={options}
loading={loading}
renderInput={params => (
<TextField
{...params}
label="User"
variant="outlined"
size="small"
name="search"
onChange={handleQueryUpdate}
InputProps={{
...params.InputProps,
startAdornment: (
<InputAdornment position="start">
<Search />
</InputAdornment>
),
endAdornment: (
<>
<ConditionallyRender
condition={loading}
show={
<CircularProgress
color="inherit"
size={20}
/>
}
/>
{params.InputProps.endAdornment}
</>
),
}}
/>
)}
/>
</Grid>
<Grid item>
<ProjectRoleSelect
labelId="add-user-select-role-label"
id="add-user-select-role"
placeholder="Project role"
value={role?.id || -1}
onChange={handleRoleChange}
roles={roles}
/>
</Grid>
<Grid item>
<Button
variant="contained"
color="primary"
disabled={!user}
onClick={handleSubmit}
>
Add user
</Button>
</Grid>
</Grid>
</>
);
};

View File

@ -0,0 +1,64 @@
import { List } from '@material-ui/core';
import {
IProjectAccessOutput,
IProjectAccessUser,
} from '../../../../hooks/api/getters/useProjectAccess/useProjectAccess';
import { ProjectAccessListItem } from './ProjectAccessListItem/ProjectAccessListItem';
interface IProjectAccesListProps {
page: IProjectAccessUser[];
handleRoleChange: (
userId: number,
currRoleId: number
) => (
evt: React.ChangeEvent<{
name?: string;
value: unknown;
}>
) => void;
handleRemoveAccess: (user: IProjectAccessUser) => void;
access: IProjectAccessOutput;
}
export const ProjectAccessList: React.FC<IProjectAccesListProps> = ({
page,
access,
handleRoleChange,
handleRemoveAccess,
children,
}) => {
const sortUsers = (users: IProjectAccessUser[]): IProjectAccessUser[] => {
/* This should be done on the API side in the future,
we should expect the list of users to come in the
same order each time and not jump around on the screen*/
return users.sort(
(userA: IProjectAccessUser, userB: IProjectAccessUser) => {
if (!userA.name) {
return -1;
} else if (!userB.name) {
return 1;
}
return userA.name.localeCompare(userB.name);
}
);
};
return (
<List>
{sortUsers(page).map(user => {
return (
<ProjectAccessListItem
key={user.id}
user={user}
access={access}
handleRoleChange={handleRoleChange}
handleRemoveAccess={handleRemoveAccess}
/>
);
})}
{children}
</List>
);
};

View File

@ -0,0 +1,11 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(() => ({
iconButton: {
marginLeft: '0.5rem',
},
actionList: {
display: 'flex',
alignItems: 'center',
},
}));

View File

@ -0,0 +1,93 @@
import {
ListItem,
ListItemAvatar,
Avatar,
ListItemText,
ListItemSecondaryAction,
MenuItem,
} from '@material-ui/core';
import { Delete } from '@material-ui/icons';
import { useParams } from 'react-router-dom';
import {
IProjectAccessUser,
IProjectAccessOutput,
} from '../../../../../hooks/api/getters/useProjectAccess/useProjectAccess';
import { IProjectViewParams } from '../../../../../interfaces/params';
import PermissionIconButton from '../../../../common/PermissionIconButton/PermissionIconButton';
import { UPDATE_PROJECT } from '../../../../providers/AccessProvider/permissions';
import { ProjectRoleSelect } from '../../ProjectRoleSelect/ProjectRoleSelect';
import { useStyles } from '../ProjectAccessListItem/ProjectAccessListItem.styles';
interface IProjectAccessListItemProps {
user: IProjectAccessUser;
handleRoleChange: (
userId: number,
currRoleId: number
) => (
evt: React.ChangeEvent<{
name?: string;
value: unknown;
}>
) => void;
handleRemoveAccess: (user: IProjectAccessUser) => void;
access: IProjectAccessOutput;
}
export const ProjectAccessListItem = ({
user,
access,
handleRoleChange,
handleRemoveAccess,
}: IProjectAccessListItemProps) => {
const { id: projectId } = useParams<IProjectViewParams>();
const styles = useStyles();
const labelId = `checkbox-list-secondary-label-${user.id}`;
return (
<ListItem key={user.id} button>
<ListItemAvatar>
<Avatar alt={user.name} src={user.imageUrl} />
</ListItemAvatar>
<ListItemText
id={labelId}
primary={user.name}
secondary={user.email || user.username}
/>
<ListItemSecondaryAction className={styles.actionList}>
<ProjectRoleSelect
labelId={`role-${user.id}-select-label`}
id={`role-${user.id}-select`}
key={user.id}
placeholder="Choose role"
onChange={handleRoleChange(user.id, user.roleId)}
roles={access.roles}
value={user.roleId || -1}
>
<MenuItem value="" disabled>
Choose role
</MenuItem>
</ProjectRoleSelect>
<PermissionIconButton
permission={UPDATE_PROJECT}
projectId={projectId}
className={styles.iconButton}
edge="end"
aria-label="delete"
title="Remove access"
onClick={() => {
handleRemoveAccess(user);
}}
disabled={access.users.length === 1}
tooltip={
access.users.length === 1
? 'A project must have at least one owner'
: 'Remove access'
}
>
<Delete />
</PermissionIconButton>
</ListItemSecondaryAction>
</ListItem>
);
};

View File

@ -1,19 +1,24 @@
import { FormControl, InputLabel, Select, MenuItem } from '@material-ui/core';
import React from 'react';
import IRole from '../../../../interfaces/role';
import { IProjectRole } from '../../../../interfaces/role';
import { useStyles } from '../ProjectAccess.styles';
interface IProjectRoleSelect {
roles: IRole[];
roles: IProjectRole[];
labelId: string;
id: string;
placeholder?: string;
onChange: () => void;
onChange: (
evt: React.ChangeEvent<{
name?: string | undefined;
value: unknown;
}>
) => void;
value: any;
}
const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
export const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
roles,
onChange,
labelId,
@ -39,9 +44,10 @@ const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
value={value || ''}
onChange={onChange}
renderValue={roleId => {
return roles?.find(role => {
const role = roles?.find(role => {
return role.id === roleId;
}).name;
});
return role?.name || '';
}}
>
{children}
@ -66,5 +72,3 @@ const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
</FormControl>
);
};
export default ProjectRoleSelect;

View File

@ -1,162 +0,0 @@
import React, { useEffect, useState } from 'react';
import projectApi from '../../store/project/api';
import PropTypes from 'prop-types';
import {
TextField,
CircularProgress,
Grid,
Button,
InputAdornment,
} from '@material-ui/core';
import { Search } from '@material-ui/icons';
import Autocomplete from '@material-ui/lab/Autocomplete';
import { Alert } from '@material-ui/lab';
import ProjectRoleSelect from './ProjectAccess/ProjectRoleSelect/ProjectRoleSelect';
function AddUserComponent({ roles, addUserToRole }) {
const [user, setUser] = useState();
const [role, setRole] = useState({});
const [options, setOptions] = useState([]);
const [loading, setLoading] = useState(false);
const [select, setSelect] = useState(false);
useEffect(() => {
if (roles.length > 0) {
const regularRole = roles.find(
r => r.name.toLowerCase() === 'regular'
);
setRole(regularRole || roles[0]);
}
}, [roles]);
const search = async q => {
if (q.length > 1) {
setLoading(true);
// TODO: Do not hard-code fetch here.
const users = await projectApi.searchProjectUser(q);
setOptions([...users]);
} else {
setOptions([]);
}
setLoading(false);
};
const handleQueryUpdate = evt => {
const q = evt.target.value;
search(q);
if (options.length === 1) {
setSelect(true);
return;
}
setSelect(false);
};
const handleSelectUser = (evt, selectedUser) => {
setOptions([]);
if (selectedUser?.id) {
setUser(selectedUser);
}
};
const handleRoleChange = evt => {
const roleId = +evt.target.value;
const role = roles.find(r => r.id === roleId);
setRole(role);
};
const handleSubmit = async evt => {
evt.preventDefault();
await addUserToRole(user.id, role.id);
setUser(undefined);
setOptions([]);
};
return (
<>
<Alert severity="info" style={{ marginBottom: '20px' }}>
The user must have an Unleash root role before added to the
project.
</Alert>
<Grid container spacing={3} alignItems="flex-end">
<Grid item>
<Autocomplete
id="add-user-component"
style={{ width: 300 }}
noOptionsText="No users found."
onChange={handleSelectUser}
autoSelect={select}
value={user || ''}
freeSolo
getOptionSelected={() => true}
filterOptions={o => o}
getOptionLabel={option => {
if (option) {
return `${option.name || '(Empty name)'} <${
option.email || option.username
}>`;
} else return '';
}}
options={options}
loading={loading}
renderInput={params => (
<TextField
{...params}
label="User"
variant="outlined"
size="small"
name="search"
onChange={handleQueryUpdate}
InputProps={{
...params.InputProps,
startAdornment: (
<InputAdornment position="start">
<Search />
</InputAdornment>
),
endAdornment: (
<React.Fragment>
{loading ? (
<CircularProgress
color="inherit"
size={20}
/>
) : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
/>
</Grid>
<Grid item>
<ProjectRoleSelect
labelId="add-user-select-role-label"
id="add-user-select-role"
placeholder="Project role"
value={role.id || ''}
onChange={handleRoleChange}
roles={roles}
/>
</Grid>
<Grid item>
<Button
variant="contained"
color="primary"
disabled={!user}
onClick={handleSubmit}
>
Add user
</Button>
</Grid>
</Grid>
</>
);
}
AddUserComponent.propTypes = {
roles: PropTypes.array.isRequired,
addUserToRole: PropTypes.func.isRequired,
};
export default AddUserComponent;

View File

@ -1,20 +0,0 @@
import { connect } from 'react-redux';
import Component from './ProjectAccess/ProjectAccess';
const mapStateToProps = (state, props) => {
const projectBase = { id: '', name: '', description: '' };
const realProject = state.projects
.toJS()
.find(n => n.id === props.projectId);
const project = Object.assign(projectBase, realProject);
return {
project,
};
};
const mapDispatchToProps = () => ({});
const AccessContainer = connect(mapStateToProps, mapDispatchToProps)(Component);
export default AccessContainer;

View File

@ -1,3 +0,0 @@
import TagTypeList from './TagTypeList';
export default TagTypeList;

View File

@ -1,157 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`it supports editMode 1`] = `
<div
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style={
Object {
"borderRadius": "10px",
"boxShadow": "none",
}
}
>
<div
className="makeStyles-headerContainer-1"
>
<div
className="makeStyles-headerTitleContainer-5"
>
<div
className=""
data-loading={true}
>
<h2
className="MuiTypography-root makeStyles-headerTitle-6 MuiTypography-h2"
>
Update Tag type
</h2>
</div>
</div>
</div>
<div
className="makeStyles-bodyContainer-2"
>
<section
className="contentSpacing tagTypeContainer"
>
<h6
className="MuiTypography-root MuiTypography-subtitle1"
>
Tag types allow you to group tags together in the management UI
</h6>
<form
className="addTagTypeForm contentSpacing"
onSubmit={[Function]}
>
<span>
You do not have permissions to save.
</span>
</form>
</section>
</div>
</div>
`;
exports[`renders correctly for creating 1`] = `
<div
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style={
Object {
"borderRadius": "10px",
"boxShadow": "none",
}
}
>
<div
className="makeStyles-headerContainer-1"
>
<div
className="makeStyles-headerTitleContainer-5"
>
<div
className=""
data-loading={true}
>
<h2
className="MuiTypography-root makeStyles-headerTitle-6 MuiTypography-h2"
>
Create Tag type
</h2>
</div>
</div>
</div>
<div
className="makeStyles-bodyContainer-2"
>
<section
className="contentSpacing tagTypeContainer"
>
<h6
className="MuiTypography-root MuiTypography-subtitle1"
>
Tag types allow you to group tags together in the management UI
</h6>
<form
className="addTagTypeForm contentSpacing"
onSubmit={[Function]}
>
<span>
You do not have permissions to save.
</span>
</form>
</section>
</div>
</div>
`;
exports[`renders correctly for creating without permissions 1`] = `
<div
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style={
Object {
"borderRadius": "10px",
"boxShadow": "none",
}
}
>
<div
className="makeStyles-headerContainer-1"
>
<div
className="makeStyles-headerTitleContainer-5"
>
<div
className=""
data-loading={true}
>
<h2
className="MuiTypography-root makeStyles-headerTitle-6 MuiTypography-h2"
>
Create Tag type
</h2>
</div>
</div>
</div>
<div
className="makeStyles-bodyContainer-2"
>
<section
className="contentSpacing tagTypeContainer"
>
<h6
className="MuiTypography-root MuiTypography-subtitle1"
>
Tag types allow you to group tags together in the management UI
</h6>
<form
className="addTagTypeForm contentSpacing"
onSubmit={[Function]}
>
<span>
You do not have permissions to save.
</span>
</form>
</section>
</div>
</div>
`;

View File

@ -1,80 +0,0 @@
import React from 'react';
import { ThemeProvider } from '@material-ui/core';
import TagTypes from '../form-tag-type-component';
import renderer from 'react-test-renderer';
import theme from '../../../themes/main-theme';
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
import { createFakeStore } from '../../../accessStoreFake';
import {
CREATE_TAG_TYPE,
UPDATE_TAG_TYPE,
} from '../../providers/AccessProvider/permissions';
jest.mock('@material-ui/core/TextField');
test('renders correctly for creating', () => {
const tree = renderer
.create(
<ThemeProvider theme={theme}>
<AccessProvider
store={createFakeStore([{ permission: CREATE_TAG_TYPE }])}
>
<TagTypes
history={{}}
title="Add tag type"
createTagType={jest.fn()}
validateName={() => Promise.resolve(true)}
tagType={{ name: '', description: '', icon: '' }}
editMode={false}
submit={jest.fn()}
/>
</AccessProvider>
</ThemeProvider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
test('renders correctly for creating without permissions', () => {
const tree = renderer
.create(
<ThemeProvider theme={theme}>
<AccessProvider store={createFakeStore([])}>
<TagTypes
history={{}}
title="Add tag type"
createTagType={jest.fn()}
validateName={() => Promise.resolve(true)}
tagType={{ name: '', description: '', icon: '' }}
editMode={false}
submit={jest.fn()}
/>
</AccessProvider>
</ThemeProvider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
test('it supports editMode', () => {
const tree = renderer
.create(
<ThemeProvider theme={theme}>
<AccessProvider
store={createFakeStore([{ permission: UPDATE_TAG_TYPE }])}
>
<TagTypes
history={{}}
title="Add tag type"
createTagType={jest.fn()}
validateName={() => Promise.resolve(true)}
tagType={{ name: '', description: '', icon: '' }}
editMode
submit={jest.fn()}
/>
</AccessProvider>
</ThemeProvider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});

View File

@ -1,17 +0,0 @@
import { connect } from 'react-redux';
import TagTypeComponent from './form-tag-type-component';
import { createTagType, validateName } from '../../store/tag-type/actions';
const mapStateToProps = () => ({
tagType: { name: '', description: '', icon: '' },
editMode: false,
});
const mapDispatchToProps = dispatch => ({
validateName: name => validateName(name),
submit: tagType => createTagType(tagType)(dispatch),
});
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(TagTypeComponent);
export default FormAddContainer;

View File

@ -1,25 +0,0 @@
import { connect } from 'react-redux';
import Component from './form-tag-type-component';
import { updateTagType } from '../../store/tag-type/actions';
const mapStateToProps = (state, props) => {
const tagTypeBase = { name: '', description: '', icon: '' };
const realTagType = state.tagTypes.toJS().find(n => n.name === props.tagTypeName);
const tagType = Object.assign(tagTypeBase, realTagType);
return {
tagType,
editMode: true,
};
};
const mapDispatchToProps = dispatch => ({
validateName: () => {},
submit: tagType => {
updateTagType(tagType)(dispatch);
},
});
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(Component);
export default FormAddContainer;

View File

@ -1,140 +0,0 @@
import React, { useContext, useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { FormButtons } from '../common';
import PageContent from '../common/PageContent/PageContent';
import { Typography, TextField } from '@material-ui/core';
import styles from './TagType.module.scss';
import commonStyles from '../common/common.module.scss';
import AccessContext from '../../contexts/AccessContext';
import {
CREATE_TAG_TYPE,
UPDATE_TAG_TYPE,
} from '../providers/AccessProvider/permissions';
import ConditionallyRender from '../common/ConditionallyRender';
const AddTagTypeComponent = ({
tagType,
validateName,
submit,
history,
editMode,
}) => {
const [tagTypeName, setTagTypeName] = useState(tagType.name || '');
const [tagTypeDescription, setTagTypeDescription] = useState(
tagType.description || ''
);
const [errors, setErrors] = useState({
general: undefined,
name: undefined,
description: undefined,
});
const { hasAccess } = useContext(AccessContext);
const onValidateName = async evt => {
evt.preventDefault();
const name = evt.target.value;
try {
await validateName(name);
setErrors({ name: undefined });
} catch (err) {
setErrors({ name: err.message });
}
};
const onCancel = evt => {
evt.preventDefault();
history.push('/tag-types');
};
const onSubmit = async evt => {
evt.preventDefault();
try {
await submit({
name: tagTypeName,
description: tagTypeDescription,
});
history.push('/tag-types');
} catch (e) {
setErrors({ general: e.message });
}
};
const submitText = editMode ? 'Update' : 'Create';
return (
<PageContent headerContent={`${submitText} Tag type`}>
<section
className={classnames(
commonStyles.contentSpacing,
styles.tagTypeContainer
)}
>
<Typography variant="subtitle1">
Tag types allow you to group tags together in the
management UI
</Typography>
<form
onSubmit={onSubmit}
className={classnames(
styles.addTagTypeForm,
commonStyles.contentSpacing
)}
>
<TextField
label="Name"
name="name"
placeholder="url-friendly-unique-name"
value={tagTypeName}
error={errors.name !== undefined}
helperText={errors.name}
disabled={editMode}
onBlur={onValidateName}
onChange={v => setTagTypeName(v.target.value.trim())}
variant="outlined"
size="small"
/>
<TextField
label="Description"
name="description"
placeholder="Some short explanation of the tag type"
rowsMax={4}
multiline
error={errors.description !== undefined}
helperText={errors.description}
value={tagTypeDescription}
onChange={v => setTagTypeDescription(v.target.value)}
variant="outlined"
size="small"
/>
<ConditionallyRender
condition={hasAccess(
editMode ? UPDATE_TAG_TYPE : CREATE_TAG_TYPE
)}
show={
<div className={styles.formButtons}>
<FormButtons
submitText={submitText}
onCancel={onCancel}
/>
</div>
}
elseShow={
<span>You do not have permissions to save.</span>
}
/>
</form>
</section>
</PageContent>
);
};
AddTagTypeComponent.propTypes = {
tagType: PropTypes.object.isRequired,
validateName: PropTypes.func.isRequired,
submit: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
editMode: PropTypes.bool.isRequired,
};
export default AddTagTypeComponent;

View File

@ -1,21 +0,0 @@
import { connect } from 'react-redux';
import TagTypesListComponent from './TagTypeList';
import { fetchTagTypes, removeTagType } from '../../store/tag-type/actions';
const mapStateToProps = state => {
const list = state.tagTypes.toJS();
return {
tagTypes: list,
};
};
const mapDispatchToProps = dispatch => ({
removeTagType: tagtype => {
removeTagType(tagtype)(dispatch);
},
fetchTagTypes: () => fetchTagTypes()(dispatch),
});
const TagTypesListContainer = connect(mapStateToProps, mapDispatchToProps)(TagTypesListComponent);
export default TagTypesListContainer;

View File

@ -5,7 +5,7 @@ import useToast from '../../../hooks/useToast';
import FormTemplate from '../../common/FormTemplate/FormTemplate';
import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { UPDATE_TAG_TYPE } from '../../providers/AccessProvider/permissions';
import useTagForm from '../hooks/useTagForm';
import useTagTypeForm from '../TagTypeForm/useTagTypeForm';
import TagTypeForm from '../TagTypeForm/TagTypeForm';
const CreateTagType = () => {
@ -21,7 +21,7 @@ const CreateTagType = () => {
validateNameUniqueness,
errors,
clearErrors,
} = useTagForm();
} = useTagTypeForm();
const { createTag, loading } = useTagTypesApi();
const handleSubmit = async (e: Event) => {

View File

@ -6,7 +6,7 @@ import useToast from '../../../hooks/useToast';
import FormTemplate from '../../common/FormTemplate/FormTemplate';
import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { UPDATE_TAG_TYPE } from '../../providers/AccessProvider/permissions';
import useTagForm from '../hooks/useTagForm';
import useTagTypeForm from '../TagTypeForm/useTagTypeForm';
import TagForm from '../TagTypeForm/TagTypeForm';
const EditTagType = () => {
@ -23,7 +23,7 @@ const EditTagType = () => {
getTagPayload,
errors,
clearErrors,
} = useTagForm(tagType?.name, tagType?.description);
} = useTagTypeForm(tagType?.name, tagType?.description);
const { updateTagType, loading } = useTagTypesApi();
const handleSubmit = async (e: Event) => {

View File

@ -1,32 +0,0 @@
.select {
min-width: 100px;
}
.textfield {
margin-left: 15px;
}
.header {
padding: var(--card-header-padding);
word-break: break-all;
border-bottom: var(--default-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.header h1 {
font-size: var(--h1-size);
}
.container {
padding: var(--card-padding);
}
.formbuttons {
padding-top: 1rem;
}
.tagListItem {
padding: 0;
}

View File

@ -1,138 +0,0 @@
import React, { useContext, useEffect } from 'react';
import PropTypes from 'prop-types';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import { useHistory } from 'react-router-dom';
import {
Button,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
Tooltip,
} from '@material-ui/core';
import { Add, Label, Delete } from '@material-ui/icons';
import {
CREATE_TAG,
DELETE_TAG,
} from '../../providers/AccessProvider/permissions';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import HeaderTitle from '../../common/HeaderTitle';
import PageContent from '../../common/PageContent/PageContent';
import { useStyles } from './TagList.styles';
import AccessContext from '../../../contexts/AccessContext';
const TagList = ({ tags, fetchTags, removeTag }) => {
const history = useHistory();
const smallScreen = useMediaQuery('(max-width:700px)');
const styles = useStyles();
const { hasAccess } = useContext(AccessContext);
useEffect(() => {
fetchTags();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const remove = (tag, evt) => {
evt.preventDefault();
removeTag(tag);
};
const listItem = tag => (
<ListItem
key={`${tag.type}_${tag.value}`}
className={styles.tagListItem}
>
<ListItemIcon>
<Label />
</ListItemIcon>
<ListItemText primary={tag.value} secondary={tag.type} />
<ConditionallyRender
condition={hasAccess(DELETE_TAG)}
show={<DeleteButton tagType={tag.type} tagValue={tag.value} />}
/>
</ListItem>
);
const DeleteButton = ({ tagType, tagValue }) => (
<Tooltip title="Delete tag">
<IconButton
onClick={e => remove({ type: tagType, value: tagValue }, e)}
>
<Delete />
</IconButton>
</Tooltip>
);
DeleteButton.propTypes = {
tagType: PropTypes.string,
tagValue: PropTypes.string,
};
const AddButton = ({ hasAccess }) => (
<ConditionallyRender
condition={hasAccess(CREATE_TAG)}
show={
<ConditionallyRender
condition={smallScreen}
show={
<IconButton
aria-label="add tag"
onClick={() => history.push('/tag-types/create')}
>
<Add />
</IconButton>
}
elseShow={
<Tooltip title="Add new tag">
<Button
color="primary"
startIcon={<Add />}
onClick={() =>
history.push('/tag-types/create')
}
variant="contained"
>
Add new tag
</Button>
</Tooltip>
}
/>
}
/>
);
return (
<PageContent
headerContent={
<HeaderTitle
title="Tags"
actions={<AddButton hasAccess={hasAccess} />}
/>
}
>
<List>
<ConditionallyRender
condition={tags.length > 0}
show={tags.map(tag => listItem(tag))}
elseShow={
<ListItem className={styles.tagListItem}>
<ListItemText primary="No tags available" />
</ListItem>
}
/>
</List>
</PageContent>
);
};
TagList.propTypes = {
tags: PropTypes.array.isRequired,
fetchTags: PropTypes.func.isRequired,
removeTag: PropTypes.func.isRequired,
};
export default TagList;

View File

@ -1,7 +0,0 @@
import { makeStyles } from '@material-ui/styles';
export const useStyles = makeStyles({
tagListItem: {
padding: 0,
},
});

View File

@ -1,3 +0,0 @@
import TagsListComponent from './TagList';
export default TagsListComponent;

View File

@ -38,7 +38,6 @@ export const useStyles = makeStyles(theme => ({
position: 'relative',
},
errorMessage: {
//@ts-ignore
fontSize: theme.fontSizes.smallBody,
color: theme.palette.error.main,
position: 'absolute',

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi';
const useTagForm = (initialTagName = '', initialTagDesc = '') => {
const useTagTypeForm = (initialTagName = '', initialTagDesc = '') => {
const [tagName, setTagName] = useState(initialTagName);
const [tagDesc, setTagDesc] = useState(initialTagDesc);
const [errors, setErrors] = useState({});
@ -66,4 +66,4 @@ const useTagForm = (initialTagName = '', initialTagDesc = '') => {
};
};
export default useTagForm;
export default useTagTypeForm;

View File

@ -2,12 +2,12 @@ import { useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { Link, useHistory } from 'react-router-dom';
import {
Button,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
IconButton,
Button,
Tooltip,
} from '@material-ui/core';
import { Add, Delete, Edit, Label } from '@material-ui/icons';
@ -20,14 +20,14 @@ import {
} from '../../providers/AccessProvider/permissions';
import Dialogue from '../../common/Dialogue/Dialogue';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import styles from '../TagType.module.scss';
import styles from './TagTypeList.module.scss';
import AccessContext from '../../../contexts/AccessContext';
import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi';
import useTagTypes from '../../../hooks/api/getters/useTagTypes/useTagTypes';
import useToast from '../../../hooks/useToast';
import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton';
const TagTypeList = () => {
export const TagTypeList = () => {
const { hasAccess } = useContext(AccessContext);
const [deletion, setDeletion] = useState({ open: false });
const history = useHistory();
@ -160,5 +160,3 @@ TagTypeList.propTypes = {
fetchTagTypes: PropTypes.func.isRequired,
removeTagType: PropTypes.func.isRequired,
};
export default TagTypeList;

View File

@ -1,20 +1,18 @@
import React from 'react';
import TagTypesList from '../TagTypeList';
import { TagTypeList } from '../TagTypeList';
import renderer from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom';
import { ThemeProvider } from '@material-ui/styles';
import theme from '../../../themes/main-theme';
import { createFakeStore } from '../../../accessStoreFake';
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
import theme from '../../../../themes/main-theme';
import { createFakeStore } from '../../../../accessStoreFake';
import AccessProvider from '../../../providers/AccessProvider/AccessProvider';
import {
ADMIN,
CREATE_TAG_TYPE,
UPDATE_TAG_TYPE,
DELETE_TAG_TYPE,
} from '../../providers/AccessProvider/permissions';
import UIProvider from '../../providers/UIProvider/UIProvider';
} from '../../../providers/AccessProvider/permissions';
import UIProvider from '../../../providers/UIProvider/UIProvider';
test('renders an empty list correctly', () => {
const tree = renderer.create(
@ -24,7 +22,7 @@ test('renders an empty list correctly', () => {
<AccessProvider
store={createFakeStore([{ permission: ADMIN }])}
>
<TagTypesList
<TagTypeList
tagTypes={[]}
fetchTagTypes={jest.fn()}
removeTagType={jest.fn()}
@ -50,7 +48,7 @@ test('renders a list with elements correctly', () => {
{ permission: DELETE_TAG_TYPE },
])}
>
<TagTypesList
<TagTypeList
tagTypes={[
{
name: 'simple',

View File

@ -1,15 +0,0 @@
import { connect } from 'react-redux';
import TagComponent from './form-tag-component';
import { create } from '../../store/tag/actions';
const mapStateToProps = () => ({
tag: { type: '', value: '' },
});
const mapDispatchToProps = dispatch => ({
submit: tag => create(tag)(dispatch),
});
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(TagComponent);
export default FormAddContainer;

View File

@ -1,107 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { TextField } from '@material-ui/core';
import styles from './Tag.module.scss';
import { FormButtons } from '../common';
import PageContent from '../common/PageContent/PageContent';
import TagSelect from '../common/TagSelect/TagSelect';
class AddTagComponent extends Component {
constructor(props) {
super(props);
this.state = {
tag: props.tag,
errors: {},
dirty: false,
currentLegalValue: '',
};
}
static getDerivedStateFromProps(props, state) {
if (!state.tag.id && props.tag.id) {
return { tag: props.tag };
} else {
return null;
}
}
setValue = (field, value) => {
const { tag } = this.state;
tag[field] = value;
this.setState({ tag, dirty: true });
};
onCancel = evt => {
evt.preventDefault();
this.props.history.push('/tags');
};
onSubmit = async evt => {
evt.preventDefault();
const { tag } = this.state;
if (!tag.type || tag.type === '') {
tag.type = 'simple';
}
try {
await this.props.submit(tag);
this.props.history.push('/tags');
} catch (e) {
this.setState({
errors: {
general: e.message,
},
});
}
};
render() {
const { tag, errors } = this.state;
const submitText = 'Create';
return (
<PageContent headerContent={`${submitText} Tag`}>
<section className={styles.container}>
<form onSubmit={this.onSubmit}>
<p style={{ color: 'red' }}>{errors.general}</p>
<TagSelect
name="type"
value={tag.type}
onChange={v =>
this.setValue('type', v.target.value)
}
className={styles.select}
/>
<TextField
label="Value"
name="value"
placeholder="Your tag"
size="small"
variant="outlined"
rowsMax={4}
value={tag.value}
error={errors.value !== undefined}
helperText={errors.value}
onChange={v =>
this.setValue('value', v.target.value)
}
className={styles.textfield}
/>
<div className={styles.formbuttons}>
<FormButtons
submitText={submitText}
onCancel={this.onCancel}
/>
</div>
</form>
</section>
</PageContent>
);
}
}
AddTagComponent.propTypes = {
tag: PropTypes.object.isRequired,
submit: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
};
export default AddTagComponent;

View File

@ -1,24 +0,0 @@
import { connect } from 'react-redux';
import TagsListComponent from './TagList';
import { fetchTags, removeTag } from '../../store/tag/actions';
const mapStateToProps = state => {
const list = state.tags.toJS();
return {
tags: list,
};
};
const mapDispatchToProps = dispatch => ({
removeTag: tag => {
// eslint-disable-next-line no-alert
if (window.confirm('Are you sure you want to remove this tag?')) {
removeTag(tag)(dispatch);
}
},
fetchTags: () => fetchTags()(dispatch),
});
const TagsListContainer = connect(mapStateToProps, mapDispatchToProps)(TagsListComponent);
export default TagsListContainer;

View File

@ -3,7 +3,7 @@ import { makeStyles } from '@material-ui/styles';
export const useStyles = makeStyles(theme => ({
profile: {
position: 'absolute',
zIndex: '5000',
zIndex: 5000,
minWidth: '300px',
right: 0,
padding: '1.5rem',

View File

@ -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',

View File

@ -163,14 +163,19 @@ const useAPI = ({
if (res.status > 399) {
const response = await res.json();
if (response?.details?.length > 0) {
if (response?.details?.length > 0 && propagateErrors) {
const error = response.details[0];
if (propagateErrors) {
throw new Error(error.message);
throw new Error(error.message || error.msg);
}
return error;
}
if (response?.length > 0 && propagateErrors) {
const error = response[0];
throw new Error(error.message || error.msg);
}
if (propagateErrors) {
throw new Error('Action could not be performed');
}

View File

@ -107,6 +107,54 @@ const useProjectApi = () => {
}
};
const addUserToRole = async (
projectId: string,
roleId: number,
userId: number
) => {
const path = `api/admin/projects/${projectId}/users/${userId}/roles/${roleId}`;
const req = createRequest(path, { method: 'POST' });
try {
const res = await makeRequest(req.caller, req.id);
return res;
} catch (e) {
throw e;
}
};
const removeUserFromRole = async (
projectId: string,
roleId: number,
userId: number
) => {
const path = `api/admin/projects/${projectId}/users/${userId}/roles/${roleId}`;
const req = createRequest(path, { method: 'DELETE' });
try {
const res = await makeRequest(req.caller, req.id);
return res;
} catch (e) {
throw e;
}
};
const searchProjectUser = async (query: string): Promise<Response> => {
const path = `api/admin/user-admin/search?q=${query}`;
const req = createRequest(path, { method: 'GET' });
try {
const res = await makeRequest(req.caller, req.id);
return res;
} catch (e) {
throw e;
}
};
return {
createProject,
validateId,
@ -114,8 +162,11 @@ const useProjectApi = () => {
deleteProject,
addEnvironmentToProject,
removeEnvironmentFromProject,
addUserToRole,
removeUserFromRole,
errors,
loading,
searchProjectUser,
};
};

View File

@ -5,15 +5,15 @@ const handleErrorResponses = (target: string) => async (res: Response) => {
);
// Try to resolve body, but don't rethrow res.json is not a function
try {
// @ts-ignore
// @ts-expect-error
error.info = await res.json();
} catch (e) {
// @ts-ignore
// @ts-expect-error
error.info = {};
}
// @ts-ignore
// @ts-expect-error
error.status = res.status;
// @ts-ignore
// @ts-expect-error
error.statusText = res.statusText;
throw error;
}

View File

@ -0,0 +1,61 @@
import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import handleErrorResponses from '../httpErrorResponseHandler';
import { IProjectRole } from '../../../../interfaces/role';
export interface IProjectAccessUser {
id: number;
imageUrl: string;
isAPI: boolean;
roleId: number;
username?: string;
name?: string;
email?: string;
}
export interface IProjectAccessOutput {
users: IProjectAccessUser[];
roles: IProjectRole[];
}
const useProjectAccess = (
projectId: string,
options: SWRConfiguration = {}
) => {
const path = formatApiPath(`api/admin/projects/${projectId}/users`);
const fetcher = () => {
return fetch(path, {
method: 'GET',
})
.then(handleErrorResponses('project access'))
.then(res => res.json());
};
const CACHE_KEY = `api/admin/projects/${projectId}/users`;
const { data, error } = useSWR<IProjectAccessOutput>(
CACHE_KEY,
fetcher,
options
);
const [loading, setLoading] = useState(!error && !data);
const refetchProjectAccess = () => {
mutate(CACHE_KEY);
};
useEffect(() => {
setLoading(!error && !data);
}, [data, error]);
return {
access: data ? data : { roles: [], users: [] },
error,
loading,
refetchProjectAccess,
};
};
export default useProjectAccess;

View File

@ -19,7 +19,7 @@ const usePagination = (
const result = paginate(dataToPaginate, limit);
setPaginatedData(result);
/* eslint-disable-next-line */
}, [data, limit]);
}, [JSON.stringify(data), limit]);
const nextPage = () => {
if (pageIndex < paginatedData.length - 1) {

View File

@ -20,7 +20,7 @@ interface IToastOptions {
}
const useToast = () => {
// @ts-ignore
// @ts-expect-error
const { setToast } = useContext(UIContext);
const hideToast = () =>

View File

@ -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;
}

View File

@ -3,3 +3,7 @@ export interface IFeatureViewParams {
featureId: string;
activeTab: string;
}
export interface IProjectViewParams {
id: string;
}

View File

@ -10,6 +10,7 @@ export interface IProjectRole {
id: number;
name: string;
description: string;
type: string;
}
export default IRole;

View File

@ -9,6 +9,14 @@ export interface IUiConfig {
versionInfo: IVersionInfo;
links: ILinks[];
disablePasswordAuth?: boolean;
toast?: IProclamationToast
}
export interface IProclamationToast {
message: string;
id: string;
severity: 'success' | 'info' | 'warning' | 'error';
link: string;
}
export interface IFlags {

View File

@ -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;

Some files were not shown because too many files have changed in this diff Show More