mirror of
https://github.com/Unleash/unleash.git
synced 2025-11-10 01:19:53 +01:00
Merge branch 'main' into refactor/applications
This commit is contained in:
commit
5a05bb44a6
@ -67,6 +67,7 @@
|
|||||||
"fetch-mock": "9.11.0",
|
"fetch-mock": "9.11.0",
|
||||||
"http-proxy-middleware": "2.0.2",
|
"http-proxy-middleware": "2.0.2",
|
||||||
"immutable": "4.0.0",
|
"immutable": "4.0.0",
|
||||||
|
"@types/lodash.clonedeep": "^4.5.6",
|
||||||
"lodash.clonedeep": "4.5.0",
|
"lodash.clonedeep": "4.5.0",
|
||||||
"lodash.flow": "3.5.0",
|
"lodash.flow": "3.5.0",
|
||||||
"node-fetch": "2.6.7",
|
"node-fetch": "2.6.7",
|
||||||
|
|||||||
@ -23,7 +23,6 @@ import ToastRenderer from './common/ToastRenderer/ToastRenderer';
|
|||||||
interface IAppProps extends RouteComponentProps {
|
interface IAppProps extends RouteComponentProps {
|
||||||
user: IAuthStatus;
|
user: IAuthStatus;
|
||||||
fetchUiBootstrap: any;
|
fetchUiBootstrap: any;
|
||||||
feedback: any;
|
|
||||||
}
|
}
|
||||||
const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
||||||
// because we need the userId when the component load.
|
// because we need the userId when the component load.
|
||||||
@ -135,7 +134,6 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
|||||||
<Redirect to="/404" />
|
<Redirect to="/404" />
|
||||||
</Switch>
|
</Switch>
|
||||||
<Feedback
|
<Feedback
|
||||||
feedbackId="pnps"
|
|
||||||
openUrl="http://feedback.unleash.run"
|
openUrl="http://feedback.unleash.run"
|
||||||
/>
|
/>
|
||||||
</LayoutPicker>
|
</LayoutPicker>
|
||||||
@ -147,10 +145,10 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
|||||||
</SWRProvider>
|
</SWRProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set state to any for now, to avoid typing up entire state object while converting to tsx.
|
// Set state to any for now, to avoid typing up entire state object while converting to tsx.
|
||||||
const mapStateToProps = (state: any) => ({
|
const mapStateToProps = (state: any) => ({
|
||||||
user: state.user.toJS(),
|
user: state.user.toJS(),
|
||||||
feedback: state.feedback,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(App);
|
export default connect(mapStateToProps)(App);
|
||||||
|
|||||||
@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Grid, FormControlLabel, Checkbox } from '@material-ui/core';
|
||||||
|
|
||||||
|
import { styles as commonStyles } from '../../../common';
|
||||||
|
import { IAddonProvider } from '../../../../interfaces/addons';
|
||||||
|
|
||||||
|
interface IAddonProps {
|
||||||
|
provider: IAddonProvider;
|
||||||
|
checkedEvents: string[];
|
||||||
|
setEventValue: (name: string) => void;
|
||||||
|
error: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonEvents = ({
|
||||||
|
provider,
|
||||||
|
checkedEvents,
|
||||||
|
setEventValue,
|
||||||
|
error,
|
||||||
|
}: IAddonProps) => {
|
||||||
|
if (!provider) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<h4>Events</h4>
|
||||||
|
<span className={commonStyles.error}>{error}</span>
|
||||||
|
<Grid container spacing={0}>
|
||||||
|
{provider.events.map(e => (
|
||||||
|
<Grid item xs={4} key={e}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={checkedEvents.includes(e)}
|
||||||
|
onChange={setEventValue(e)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={e}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,23 +1,29 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { TextField, FormControlLabel, Switch } from '@material-ui/core';
|
import { TextField, FormControlLabel, Switch } from '@material-ui/core';
|
||||||
|
import { FormButtons, styles as commonStyles } from '../../common';
|
||||||
import { FormButtons, styles as commonStyles } from '../common';
|
import { trim } from '../../common/util';
|
||||||
import { trim } from '../common/util';
|
import { AddonParameters } from './AddonParameters/AddonParameters';
|
||||||
import AddonParameters from './form-addon-parameters';
|
import { AddonEvents } from './AddonEvents/AddonEvents';
|
||||||
import AddonEvents from './form-addon-events';
|
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
|
import PageContent from '../../common/PageContent/PageContent';
|
||||||
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 { useHistory } from 'react-router-dom';
|
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 { createAddon, updateAddon } = useAddonsApi();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
const [config, setConfig] = useState(addon);
|
const [config, setConfig] = useState(addon);
|
||||||
const [errors, setErrors] = useState({
|
const [errors, setErrors] = useState({
|
||||||
@ -116,6 +122,7 @@ const AddonFormComponent = ({ editMode, provider, addon, fetch }) => {
|
|||||||
history.push('/addons');
|
history.push('/addons');
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
confetti: true,
|
||||||
title: 'Addon created successfully',
|
title: 'Addon created successfully',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -196,14 +203,17 @@ const AddonFormComponent = ({ editMode, provider, addon, fetch }) => {
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<section className={styles.formSection}>
|
<section className={styles.formSection}>
|
||||||
<FormButtons submitText={submitText} onCancel={handleCancel} />
|
<FormButtons
|
||||||
|
submitText={submitText}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
AddonFormComponent.propTypes = {
|
AddonForm.propTypes = {
|
||||||
provider: PropTypes.object,
|
provider: PropTypes.object,
|
||||||
addon: PropTypes.object.isRequired,
|
addon: PropTypes.object.isRequired,
|
||||||
fetch: PropTypes.func.isRequired,
|
fetch: PropTypes.func.isRequired,
|
||||||
@ -211,5 +221,3 @@ AddonFormComponent.propTypes = {
|
|||||||
cancel: PropTypes.func.isRequired,
|
cancel: PropTypes.func.isRequired,
|
||||||
editMode: PropTypes.bool.isRequired,
|
editMode: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddonFormComponent;
|
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
import { TextField } from '@material-ui/core';
|
||||||
|
import {
|
||||||
|
IAddonConfig,
|
||||||
|
IAddonProvider,
|
||||||
|
IAddonProviderParams,
|
||||||
|
} from '../../../../../interfaces/addons';
|
||||||
|
|
||||||
|
const resolveType = ({ type = 'text', sensitive = false }, value: string) => {
|
||||||
|
if (sensitive && value === MASKED_VALUE) {
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
if (type === 'textfield') {
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MASKED_VALUE = '*****';
|
||||||
|
|
||||||
|
interface IAddonParameterProps {
|
||||||
|
provider: IAddonProvider;
|
||||||
|
errors: Record<string, string>;
|
||||||
|
definition: IAddonProviderParams;
|
||||||
|
setParameterValue: (param: string) => void;
|
||||||
|
config: IAddonConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonParameter = ({
|
||||||
|
definition,
|
||||||
|
config,
|
||||||
|
errors,
|
||||||
|
setParameterValue,
|
||||||
|
}: IAddonParameterProps) => {
|
||||||
|
const value = config.parameters[definition.name] || '';
|
||||||
|
const type = resolveType(definition, value);
|
||||||
|
const error = errors.parameters[definition.name];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: '80%', marginTop: '25px' }}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
rows={definition.type === 'textfield' ? 9 : 0}
|
||||||
|
multiline={definition.type === 'textfield'}
|
||||||
|
type={type}
|
||||||
|
label={definition.displayName}
|
||||||
|
name={definition.name}
|
||||||
|
placeholder={definition.placeholder || ''}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true,
|
||||||
|
}}
|
||||||
|
value={value}
|
||||||
|
error={error}
|
||||||
|
onChange={setParameterValue(definition.name)}
|
||||||
|
variant="outlined"
|
||||||
|
helperText={definition.description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { IAddonConfig, IAddonProvider } from '../../../../interfaces/addons';
|
||||||
|
import { AddonParameter } from './AddonParameter/AddonParameter';
|
||||||
|
|
||||||
|
interface IAddonParametersProps {
|
||||||
|
provider: IAddonProvider;
|
||||||
|
errors: Record<string, string>;
|
||||||
|
editMode: boolean;
|
||||||
|
setParameterValue: (param: string) => void;
|
||||||
|
config: IAddonConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonParameters = ({
|
||||||
|
provider,
|
||||||
|
config,
|
||||||
|
errors,
|
||||||
|
setParameterValue,
|
||||||
|
editMode,
|
||||||
|
}: IAddonParametersProps) => {
|
||||||
|
if (!provider) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<h4>Parameters</h4>
|
||||||
|
{editMode ? (
|
||||||
|
<p>
|
||||||
|
Sensitive parameters will be masked with value "<i>*****</i>
|
||||||
|
". If you don't change the value they will not be updated
|
||||||
|
when saving.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{provider.parameters.map(parameter => (
|
||||||
|
<AddonParameter
|
||||||
|
key={parameter.name}
|
||||||
|
definition={parameter}
|
||||||
|
errors={errors}
|
||||||
|
config={config}
|
||||||
|
setParameterValue={setParameterValue}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,12 +1,9 @@
|
|||||||
import { useContext, useEffect } from 'react';
|
import { ReactElement } from 'react';
|
||||||
import ConfiguredAddons from './ConfiguredAddons';
|
import { ConfiguredAddons } from './ConfiguredAddons/ConfiguredAddons';
|
||||||
import AvailableAddons from './AvailableAddons';
|
import { AvailableAddons } from './AvailableAddons/AvailableAddons';
|
||||||
import { Avatar } from '@material-ui/core';
|
import { Avatar } from '@material-ui/core';
|
||||||
import { DeviceHub } from '@material-ui/icons';
|
import { DeviceHub } from '@material-ui/icons';
|
||||||
|
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
import AccessContext from '../../../contexts/AccessContext';
|
|
||||||
|
|
||||||
import slackIcon from '../../../assets/icons/slack.svg';
|
import slackIcon from '../../../assets/icons/slack.svg';
|
||||||
import jiraIcon from '../../../assets/icons/jira.svg';
|
import jiraIcon from '../../../assets/icons/jira.svg';
|
||||||
import webhooksIcon from '../../../assets/icons/webhooks.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 dataDogIcon from '../../../assets/icons/datadog.svg';
|
||||||
import { formatAssetPath } from '../../../utils/format-path';
|
import { formatAssetPath } from '../../../utils/format-path';
|
||||||
import useAddons from '../../../hooks/api/getters/useAddons/useAddons';
|
import useAddons from '../../../hooks/api/getters/useAddons/useAddons';
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
width: '40px',
|
width: '40px',
|
||||||
@ -23,7 +19,7 @@ const style = {
|
|||||||
float: 'left',
|
float: 'left',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIcon = name => {
|
const getAddonIcon = (name: string): ReactElement => {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'slack':
|
case 'slack':
|
||||||
return (
|
return (
|
||||||
@ -74,40 +70,21 @@ const getIcon = name => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddonList = () => {
|
export const AddonList = () => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { providers, addons } = useAddons();
|
||||||
const { addons, providers, refetchAddons } = useAddons();
|
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (addons.length === 0) {
|
|
||||||
refetchAddons();
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [addons.length]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={addons.length > 0}
|
condition={addons.length > 0}
|
||||||
show={
|
show={<ConfiguredAddons getAddonIcon={getAddonIcon} />}
|
||||||
<ConfiguredAddons
|
|
||||||
addons={addons}
|
|
||||||
hasAccess={hasAccess}
|
|
||||||
getIcon={getIcon}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<AvailableAddons
|
<AvailableAddons
|
||||||
providers={providers}
|
providers={providers}
|
||||||
hasAccess={hasAccess}
|
getAddonIcon={getAddonIcon}
|
||||||
history={history}
|
|
||||||
getIcon={getIcon}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddonList;
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PageContent from '../../../common/PageContent/PageContent';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemAvatar,
|
|
||||||
ListItemSecondaryAction,
|
|
||||||
ListItemText,
|
|
||||||
} from '@material-ui/core';
|
|
||||||
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { CREATE_ADDON } from '../../../providers/AccessProvider/permissions';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const AvailableAddons = ({ providers, getIcon, hasAccess, history }) => {
|
|
||||||
|
|
||||||
const renderProvider = provider => (
|
|
||||||
<ListItem key={provider.name}>
|
|
||||||
<ListItemAvatar>{getIcon(provider.name)}</ListItemAvatar>
|
|
||||||
<ListItemText
|
|
||||||
primary={provider.displayName}
|
|
||||||
secondary={provider.description}
|
|
||||||
/>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={hasAccess(CREATE_ADDON)}
|
|
||||||
show={
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
name="device_hub"
|
|
||||||
onClick={() =>
|
|
||||||
history.push(`/addons/create/${provider.name}`)
|
|
||||||
}
|
|
||||||
title="Configure"
|
|
||||||
>
|
|
||||||
Configure
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<PageContent headerContent="Available addons">
|
|
||||||
<List>{providers.map(provider => renderProvider(provider))}</List>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
AvailableAddons.propTypes = {
|
|
||||||
providers: PropTypes.array.isRequired,
|
|
||||||
getIcon: PropTypes.func.isRequired,
|
|
||||||
hasAccess: PropTypes.func.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AvailableAddons;
|
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import { ReactElement } from 'react';
|
||||||
|
import PageContent from '../../../common/PageContent/PageContent';
|
||||||
|
import {
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemSecondaryAction,
|
||||||
|
ListItemText,
|
||||||
|
} from '@material-ui/core';
|
||||||
|
import { CREATE_ADDON } from '../../../providers/AccessProvider/permissions';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
|
||||||
|
|
||||||
|
interface IProvider {
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
|
documentationUrl: string;
|
||||||
|
parameters: object[];
|
||||||
|
events: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAvailableAddonsProps {
|
||||||
|
getAddonIcon: (name: string) => ReactElement;
|
||||||
|
providers: IProvider[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AvailableAddons = ({
|
||||||
|
providers,
|
||||||
|
getAddonIcon,
|
||||||
|
}: IAvailableAddonsProps) => {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const renderProvider = (provider: IProvider) => (
|
||||||
|
<ListItem key={provider.name}>
|
||||||
|
<ListItemAvatar>{getAddonIcon(provider.name)}</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={provider.displayName}
|
||||||
|
secondary={provider.description}
|
||||||
|
/>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<PermissionButton
|
||||||
|
permission={CREATE_ADDON}
|
||||||
|
onClick={() =>
|
||||||
|
history.push(`/addons/create/${provider.name}`)
|
||||||
|
}
|
||||||
|
tooltip={`Configure ${provider.name} Addon`}
|
||||||
|
>
|
||||||
|
Configure
|
||||||
|
</PermissionButton>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<PageContent headerContent="Available addons">
|
||||||
|
<List>
|
||||||
|
{providers.map((provider: IProvider) =>
|
||||||
|
renderProvider(provider)
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import AvailableAddons from './AvailableAddons';
|
|
||||||
|
|
||||||
export default AvailableAddons;
|
|
||||||
@ -1,6 +1,4 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
import {
|
||||||
IconButton,
|
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
@ -8,7 +6,6 @@ import {
|
|||||||
ListItemText,
|
ListItemText,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import { Visibility, VisibilityOff, Delete } from '@material-ui/icons';
|
import { Visibility, VisibilityOff, Delete } from '@material-ui/icons';
|
||||||
|
|
||||||
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
import {
|
import {
|
||||||
DELETE_ADDON,
|
DELETE_ADDON,
|
||||||
@ -16,17 +13,43 @@ import {
|
|||||||
} from '../../../providers/AccessProvider/permissions';
|
} from '../../../providers/AccessProvider/permissions';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import PageContent from '../../../common/PageContent/PageContent';
|
import PageContent from '../../../common/PageContent/PageContent';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import useAddons from '../../../../hooks/api/getters/useAddons/useAddons';
|
import useAddons from '../../../../hooks/api/getters/useAddons/useAddons';
|
||||||
import useToast from '../../../../hooks/useToast';
|
import useToast from '../../../../hooks/useToast';
|
||||||
import useAddonsApi from '../../../../hooks/api/actions/useAddonsApi/useAddonsApi';
|
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 }) => {
|
interface IConfigureAddonsProps {
|
||||||
const { refetchAddons } = useAddons();
|
getAddonIcon: (name: string) => ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => {
|
||||||
|
const { refetchAddons, addons } = useAddons();
|
||||||
const { updateAddon, removeAddon } = useAddonsApi();
|
const { updateAddon, removeAddon } = useAddonsApi();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
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 {
|
try {
|
||||||
await updateAddon({ ...addon, enabled: !addon.enabled });
|
await updateAddon({ ...addon, enabled: !addon.enabled });
|
||||||
refetchAddons();
|
refetchAddons();
|
||||||
@ -35,12 +58,12 @@ const ConfiguredAddons = ({ addons, hasAccess, getIcon }) => {
|
|||||||
title: 'Success',
|
title: 'Success',
|
||||||
text: 'Addon state switched successfully',
|
text: 'Addon state switched successfully',
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
setToastApiError(e.toString());
|
setToastApiError(e.toString());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRemoveAddon = addon => async () => {
|
const onRemoveAddon = async (addon: IAddon) => {
|
||||||
try {
|
try {
|
||||||
await removeAddon(addon.id);
|
await removeAddon(addon.id);
|
||||||
refetchAddons();
|
refetchAddons();
|
||||||
@ -58,9 +81,9 @@ const ConfiguredAddons = ({ addons, hasAccess, getIcon }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAddon = addon => (
|
const renderAddon = (addon: IAddon) => (
|
||||||
<ListItem key={addon.id}>
|
<ListItem key={addon.id}>
|
||||||
<ListItemAvatar>{getIcon(addon.provider)}</ListItemAvatar>
|
<ListItemAvatar>{getAddonIcon(addon.provider)}</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={
|
primary={
|
||||||
<span>
|
<span>
|
||||||
@ -85,14 +108,9 @@ const ConfiguredAddons = ({ addons, hasAccess, getIcon }) => {
|
|||||||
secondary={addon.description}
|
secondary={addon.description}
|
||||||
/>
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<ConditionallyRender
|
<PermissionIconButton
|
||||||
condition={hasAccess(UPDATE_ADDON)}
|
permission={UPDATE_ADDON}
|
||||||
show={
|
tooltip={addon.enabled ? 'Disable addon' : 'Enable addon'}
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
title={
|
|
||||||
addon.enabled ? 'Disable addon' : 'Enable addon'
|
|
||||||
}
|
|
||||||
onClick={() => toggleAddon(addon)}
|
onClick={() => toggleAddon(addon)}
|
||||||
>
|
>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -100,35 +118,38 @@ const ConfiguredAddons = ({ addons, hasAccess, getIcon }) => {
|
|||||||
show={<Visibility />}
|
show={<Visibility />}
|
||||||
elseShow={<VisibilityOff />}
|
elseShow={<VisibilityOff />}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</PermissionIconButton>
|
||||||
}
|
<PermissionIconButton
|
||||||
/>
|
permission={DELETE_ADDON}
|
||||||
<ConditionallyRender
|
tooltip={'Remove Addon'}
|
||||||
condition={hasAccess(DELETE_ADDON)}
|
onClick={() => {
|
||||||
show={
|
setDeletedAddon(addon);
|
||||||
<IconButton
|
setShowDelete(true);
|
||||||
size="small"
|
}}
|
||||||
title="Remove addon"
|
|
||||||
onClick={onRemoveAddon(addon)}
|
|
||||||
>
|
>
|
||||||
<Delete />
|
<Delete />
|
||||||
</IconButton>
|
</PermissionIconButton>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<PageContent headerContent="Configured addons">
|
<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>
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
ConfiguredAddons.propTypes = {
|
|
||||||
addons: PropTypes.array.isRequired,
|
|
||||||
hasAccess: PropTypes.func.isRequired,
|
|
||||||
toggleAddon: PropTypes.func.isRequired,
|
|
||||||
getIcon: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConfiguredAddons;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import ConfiguredAddons from './ConfiguredAddons';
|
|
||||||
|
|
||||||
export default ConfiguredAddons;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import AddonListComponent from './AddonList';
|
|
||||||
|
|
||||||
export default AddonListComponent;
|
|
||||||
42
frontend/src/component/addons/CreateAddon/CreateAddon.tsx
Normal file
42
frontend/src/component/addons/CreateAddon/CreateAddon.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import useAddons from '../../../hooks/api/getters/useAddons/useAddons';
|
||||||
|
import { AddonForm } from '../AddonForm/AddonForm';
|
||||||
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
|
|
||||||
|
interface IAddonCreateParams {
|
||||||
|
providerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_DATA = {
|
||||||
|
provider: '',
|
||||||
|
description: '',
|
||||||
|
enabled: true,
|
||||||
|
parameters: {},
|
||||||
|
events: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreateAddon = () => {
|
||||||
|
const { providerId } = useParams<IAddonCreateParams>();
|
||||||
|
|
||||||
|
const { providers, refetchAddons } = useAddons();
|
||||||
|
|
||||||
|
const editMode = false;
|
||||||
|
const provider = providers.find(
|
||||||
|
(providerItem: any) => providerItem.name === providerId
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultAddon = {
|
||||||
|
...cloneDeep(DEFAULT_DATA),
|
||||||
|
provider: provider ? provider.name : '',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
// @ts-expect-error
|
||||||
|
<AddonForm
|
||||||
|
editMode={editMode}
|
||||||
|
provider={provider}
|
||||||
|
fetch={refetchAddons}
|
||||||
|
addon={defaultAddon}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
41
frontend/src/component/addons/EditAddon/EditAddon.tsx
Normal file
41
frontend/src/component/addons/EditAddon/EditAddon.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import useAddons from '../../../hooks/api/getters/useAddons/useAddons';
|
||||||
|
import { AddonForm } from '../AddonForm/AddonForm';
|
||||||
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
|
import { IAddon } from '../../../interfaces/addons';
|
||||||
|
|
||||||
|
interface IAddonEditParams {
|
||||||
|
addonId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_DATA = {
|
||||||
|
provider: '',
|
||||||
|
description: '',
|
||||||
|
enabled: true,
|
||||||
|
parameters: {},
|
||||||
|
events: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditAddon = () => {
|
||||||
|
const { addonId } = useParams<IAddonEditParams>();
|
||||||
|
|
||||||
|
const { providers, addons, refetchAddons } = useAddons();
|
||||||
|
|
||||||
|
const editMode = true;
|
||||||
|
const addon = addons.find(
|
||||||
|
(addon: IAddon) => addon.id === Number(addonId)
|
||||||
|
) || { ...cloneDeep(DEFAULT_DATA) };
|
||||||
|
const provider = addon
|
||||||
|
? providers.find(provider => provider.name === addon.provider)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
// @ts-expect-error
|
||||||
|
<AddonForm
|
||||||
|
editMode={editMode}
|
||||||
|
provider={provider}
|
||||||
|
fetch={refetchAddons}
|
||||||
|
addon={addon}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,17 +0,0 @@
|
|||||||
.nameInput {
|
|
||||||
margin-right: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formContainer {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
max-width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formSection {
|
|
||||||
padding: 10px 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
font-size: var(--h1-size);
|
|
||||||
padding: var(--card-header-padding);
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import FormComponent from './form-addon-component';
|
|
||||||
import { updateAddon, createAddon, fetchAddons } from '../../store/addons/actions';
|
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
|
||||||
|
|
||||||
// Required for to fill the initial form.
|
|
||||||
const DEFAULT_DATA = {
|
|
||||||
provider: '',
|
|
||||||
description: '',
|
|
||||||
enabled: true,
|
|
||||||
parameters: {},
|
|
||||||
events: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = (state, params) => {
|
|
||||||
const defaultAddon = cloneDeep(DEFAULT_DATA);
|
|
||||||
const editMode = !!params.addonId;
|
|
||||||
const addons = state.addons.get('addons').toJS();
|
|
||||||
const providers = state.addons.get('providers').toJS();
|
|
||||||
|
|
||||||
let addon;
|
|
||||||
let provider;
|
|
||||||
|
|
||||||
if (editMode) {
|
|
||||||
addon = addons.find(addon => addon.id === +params.addonId) || defaultAddon;
|
|
||||||
provider = addon ? providers.find(provider => provider.name === addon.provider) : undefined;
|
|
||||||
} else {
|
|
||||||
provider = providers.find(provider => provider.name === params.provider);
|
|
||||||
addon = { ...defaultAddon, provider: provider ? provider.name : '' };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
provider,
|
|
||||||
addon,
|
|
||||||
editMode,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, ownProps) => {
|
|
||||||
const { addonId, history } = ownProps;
|
|
||||||
const submit = addonId ? updateAddon : createAddon;
|
|
||||||
|
|
||||||
return {
|
|
||||||
submit: async addonConfig => {
|
|
||||||
await submit(addonConfig)(dispatch);
|
|
||||||
history.push('/addons');
|
|
||||||
},
|
|
||||||
fetch: () => fetchAddons()(dispatch),
|
|
||||||
cancel: () => history.push('/addons'),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(FormComponent);
|
|
||||||
|
|
||||||
export default FormAddContainer;
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Grid, FormControlLabel, Checkbox } from '@material-ui/core';
|
|
||||||
|
|
||||||
import { styles as commonStyles } from '../common';
|
|
||||||
|
|
||||||
const AddonEvents = ({ provider, checkedEvents, setEventValue, error }) => {
|
|
||||||
if (!provider) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<h4>Events</h4>
|
|
||||||
<span className={commonStyles.error}>{error}</span>
|
|
||||||
<Grid container spacing={0}>
|
|
||||||
{provider.events.map(e => (
|
|
||||||
<Grid item xs={4} key={e}>
|
|
||||||
<FormControlLabel
|
|
||||||
control={<Checkbox checked={checkedEvents.includes(e)} onChange={setEventValue(e)} />}
|
|
||||||
label={e}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
AddonEvents.propTypes = {
|
|
||||||
provider: PropTypes.object,
|
|
||||||
checkedEvents: PropTypes.array.isRequired,
|
|
||||||
setEventValue: PropTypes.func.isRequired,
|
|
||||||
error: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddonEvents;
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { TextField } from '@material-ui/core';
|
|
||||||
|
|
||||||
const MASKED_VALUE = '*****';
|
|
||||||
|
|
||||||
const resolveType = ({ type = 'text', sensitive = false }, value) => {
|
|
||||||
if (sensitive && value === MASKED_VALUE) {
|
|
||||||
return 'text';
|
|
||||||
}
|
|
||||||
if (type === 'textfield') {
|
|
||||||
return 'text';
|
|
||||||
}
|
|
||||||
return type;
|
|
||||||
};
|
|
||||||
|
|
||||||
const AddonParameter = ({ definition, config, errors, setParameterValue }) => {
|
|
||||||
const value = config.parameters[definition.name] || '';
|
|
||||||
const type = resolveType(definition, value);
|
|
||||||
const error = errors.parameters[definition.name];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ width: '80%', marginTop: '25px' }}>
|
|
||||||
<TextField
|
|
||||||
size="small"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
rows={definition.type === 'textfield' ? 9 : 0}
|
|
||||||
multiline={definition.type === 'textfield'}
|
|
||||||
type={type}
|
|
||||||
label={definition.displayName}
|
|
||||||
name={definition.name}
|
|
||||||
placeholder={definition.placeholder || ''}
|
|
||||||
InputLabelProps={{
|
|
||||||
shrink: true,
|
|
||||||
}}
|
|
||||||
value={value}
|
|
||||||
error={error}
|
|
||||||
onChange={setParameterValue(definition.name)}
|
|
||||||
variant="outlined"
|
|
||||||
helperText={definition.description}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
AddonParameter.propTypes = {
|
|
||||||
definition: PropTypes.object.isRequired,
|
|
||||||
config: PropTypes.object.isRequired,
|
|
||||||
errors: PropTypes.object.isRequired,
|
|
||||||
setParameterValue: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const AddonParameters = ({ provider, config, errors, setParameterValue, editMode }) => {
|
|
||||||
if (!provider) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<h4>Parameters</h4>
|
|
||||||
{editMode ? (
|
|
||||||
<p>
|
|
||||||
Sensitive parameters will be masked with value "<i>*****</i>
|
|
||||||
". If you don't change the value they will not be updated when saving.
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
{provider.parameters.map(p => (
|
|
||||||
<AddonParameter
|
|
||||||
key={p.name}
|
|
||||||
definition={p}
|
|
||||||
errors={errors}
|
|
||||||
config={config}
|
|
||||||
setParameterValue={setParameterValue}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
AddonParameters.propTypes = {
|
|
||||||
provider: PropTypes.object,
|
|
||||||
config: PropTypes.object.isRequired,
|
|
||||||
errors: PropTypes.object.isRequired,
|
|
||||||
setParameterValue: PropTypes.func.isRequired,
|
|
||||||
editMode: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddonParameters;
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import AddonsListComponent from './AddonList';
|
|
||||||
import { fetchAddons, removeAddon, updateAddon } from '../../store/addons/actions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
const list = state.addons.toJS();
|
|
||||||
|
|
||||||
return {
|
|
||||||
addons: list.addons,
|
|
||||||
providers: list.providers,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
removeAddon: addon => {
|
|
||||||
// eslint-disable-next-line no-alert
|
|
||||||
if (window.confirm('Are you sure you want to remove this addon?')) {
|
|
||||||
removeAddon(addon)(dispatch);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fetchAddons: () => fetchAddons()(dispatch),
|
|
||||||
toggleAddon: addon => {
|
|
||||||
const updatedAddon = { ...addon, enabled: !addon.enabled };
|
|
||||||
return updateAddon(updatedAddon)(dispatch);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const AddonsListContainer = connect(mapStateToProps, mapDispatchToProps)(AddonsListComponent);
|
|
||||||
|
|
||||||
export default AddonsListContainer;
|
|
||||||
@ -12,7 +12,6 @@ import { useState } from 'react';
|
|||||||
import { scrollToTop } from '../../../common/util';
|
import { scrollToTop } from '../../../common/util';
|
||||||
|
|
||||||
const CreateApiToken = () => {
|
const CreateApiToken = () => {
|
||||||
/* @ts-ignore */
|
|
||||||
const { setToastApiError } = useToast();
|
const { setToastApiError } = useToast();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import PermissionButton from '../../../common/PermissionButton/PermissionButton'
|
|||||||
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
||||||
|
|
||||||
const CreateProjectRole = () => {
|
const CreateProjectRole = () => {
|
||||||
/* @ts-ignore */
|
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import PermissionButton from '../../../common/PermissionButton/PermissionButton'
|
|||||||
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
||||||
|
|
||||||
const CreateUser = () => {
|
const CreateUser = () => {
|
||||||
/* @ts-ignore */
|
|
||||||
const { setToastApiError } = useToast();
|
const { setToastApiError } = useToast();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|||||||
@ -38,7 +38,6 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
},
|
},
|
||||||
errorMessage: {
|
errorMessage: {
|
||||||
//@ts-ignore
|
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@ -4,7 +4,7 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
feedback: {
|
feedback: {
|
||||||
borderRadius: '12.5px',
|
borderRadius: '12.5px',
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
zIndex: '9999',
|
zIndex: 9999,
|
||||||
boxShadow: '2px 2px 4px 4px rgba(143,143,143, 0.25)',
|
boxShadow: '2px 2px 4px 4px rgba(143,143,143, 0.25)',
|
||||||
padding: '1.5rem',
|
padding: '1.5rem',
|
||||||
maxWidth: '400px',
|
maxWidth: '400px',
|
||||||
|
|||||||
@ -9,24 +9,21 @@ import { useStyles } from './Feedback.styles';
|
|||||||
import AnimateOnMount from '../AnimateOnMount/AnimateOnMount';
|
import AnimateOnMount from '../AnimateOnMount/AnimateOnMount';
|
||||||
import ConditionallyRender from '../ConditionallyRender';
|
import ConditionallyRender from '../ConditionallyRender';
|
||||||
import { formatApiPath } from '../../../utils/format-path';
|
import { formatApiPath } from '../../../utils/format-path';
|
||||||
import { Action, Dispatch } from 'redux';
|
|
||||||
import UIContext from '../../../contexts/UIContext';
|
import UIContext from '../../../contexts/UIContext';
|
||||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
||||||
|
import { PNPS_FEEDBACK_ID, showPnpsFeedback } from '../util';
|
||||||
|
|
||||||
interface IFeedbackProps {
|
interface IFeedbackProps {
|
||||||
show?: boolean;
|
|
||||||
hideFeedback: () => Dispatch<Action>;
|
|
||||||
fetchUser: () => void;
|
|
||||||
feedbackId: string;
|
|
||||||
openUrl: string;
|
openUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Feedback = ({ feedbackId, openUrl }: IFeedbackProps) => {
|
const Feedback = ({ openUrl }: IFeedbackProps) => {
|
||||||
const { showFeedback, setShowFeedback } = useContext(UIContext);
|
const { showFeedback, setShowFeedback } = useContext(UIContext);
|
||||||
const { refetch, feedback } = useUser();
|
const { refetch, feedback } = useUser();
|
||||||
const [answeredNotNow, setAnsweredNotNow] = useState(false);
|
const [answeredNotNow, setAnsweredNotNow] = useState(false);
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
|
const feedbackId = PNPS_FEEDBACK_ID;
|
||||||
|
|
||||||
const onConfirm = async () => {
|
const onConfirm = async () => {
|
||||||
const url = formatApiPath('api/admin/feedback');
|
const url = formatApiPath('api/admin/feedback');
|
||||||
@ -41,7 +38,8 @@ const Feedback = ({ feedbackId, openUrl }: IFeedbackProps) => {
|
|||||||
body: JSON.stringify({ feedbackId }),
|
body: JSON.stringify({ feedbackId }),
|
||||||
});
|
});
|
||||||
await refetch();
|
await refetch();
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
console.warn(err);
|
||||||
setShowFeedback(false);
|
setShowFeedback(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +66,8 @@ const Feedback = ({ feedbackId, openUrl }: IFeedbackProps) => {
|
|||||||
body: JSON.stringify({ feedbackId, neverShow: true }),
|
body: JSON.stringify({ feedbackId, neverShow: true }),
|
||||||
});
|
});
|
||||||
await refetch();
|
await refetch();
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
console.warn(err);
|
||||||
setShowFeedback(false);
|
setShowFeedback(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,9 +76,7 @@ const Feedback = ({ feedbackId, openUrl }: IFeedbackProps) => {
|
|||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const pnps = feedback.find(feedback => feedback.feedbackId === feedbackId);
|
if (!showPnpsFeedback(feedback)) {
|
||||||
|
|
||||||
if (pnps?.given || pnps?.neverShow) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,6 @@ const FormTemplate: React.FC<ICreateProps> = ({
|
|||||||
loading,
|
loading,
|
||||||
formatApiCode,
|
formatApiCode,
|
||||||
}) => {
|
}) => {
|
||||||
// @ts-ignore-next-line
|
|
||||||
const { setToastData } = useToast();
|
const { setToastData } = useToast();
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const smallScreen = useMediaQuery(`(max-width:${900}px)`);
|
const smallScreen = useMediaQuery(`(max-width:${900}px)`);
|
||||||
|
|||||||
@ -15,6 +15,7 @@ interface IPaginateUIProps {
|
|||||||
prevPage: () => void;
|
prevPage: () => void;
|
||||||
setPageIndex: (idx: number) => void;
|
setPageIndex: (idx: number) => void;
|
||||||
nextPage: () => void;
|
nextPage: () => void;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PaginateUI = ({
|
const PaginateUI = ({
|
||||||
|
|||||||
@ -3,13 +3,19 @@ import { useContext } from 'react';
|
|||||||
import AccessContext from '../../../contexts/AccessContext';
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
|
||||||
interface IPermissionIconButtonProps
|
interface IPermissionIconButtonProps
|
||||||
extends React.HTMLProps<HTMLButtonElement> {
|
extends React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLButtonElement>,
|
||||||
|
HTMLButtonElement
|
||||||
|
> {
|
||||||
permission: string;
|
permission: string;
|
||||||
Icon?: React.ElementType;
|
Icon?: React.ElementType;
|
||||||
tooltip: string;
|
tooltip: string;
|
||||||
onClick?: (e: any) => void;
|
onClick?: (e: any) => void;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
environmentId?: string;
|
environmentId?: string;
|
||||||
|
edge?: string;
|
||||||
|
className?: string;
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PermissionIconButton: React.FC<IPermissionIconButtonProps> = ({
|
const PermissionIconButton: React.FC<IPermissionIconButtonProps> = ({
|
||||||
|
|||||||
@ -3,16 +3,10 @@ import { Alert } from '@material-ui/lab';
|
|||||||
import ConditionallyRender from '../ConditionallyRender';
|
import ConditionallyRender from '../ConditionallyRender';
|
||||||
import { Typography } from '@material-ui/core';
|
import { Typography } from '@material-ui/core';
|
||||||
import { useStyles } from './Proclamation.styles';
|
import { useStyles } from './Proclamation.styles';
|
||||||
|
import { IProclamationToast } from '../../../interfaces/uiConfig';
|
||||||
|
|
||||||
interface IProclamationProps {
|
interface IProclamationProps {
|
||||||
toast?: IToast;
|
toast?: IProclamationToast;
|
||||||
}
|
|
||||||
|
|
||||||
interface IToast {
|
|
||||||
message: string;
|
|
||||||
id: string;
|
|
||||||
severity: 'success' | 'info' | 'warning' | 'error';
|
|
||||||
link: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderProclamation = (id: string) => {
|
const renderProclamation = (id: string) => {
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
right: '0px',
|
right: '0px',
|
||||||
bottom: '0px',
|
bottom: '0px',
|
||||||
padding: '2rem 0',
|
padding: '2rem 0',
|
||||||
zIndex: '500',
|
zIndex: 500,
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import ConditionallyRender from '../../ConditionallyRender';
|
|||||||
import Close from '@material-ui/icons/Close';
|
import Close from '@material-ui/icons/Close';
|
||||||
|
|
||||||
const Toast = ({ title, text, type, confetti }: IToastData) => {
|
const Toast = ({ title, text, type, confetti }: IToastData) => {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
const { setToast } = useContext(UIContext);
|
const { setToast } = useContext(UIContext);
|
||||||
|
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import AnimateOnMount from '../AnimateOnMount/AnimateOnMount';
|
|||||||
import Toast from './Toast/Toast';
|
import Toast from './Toast/Toast';
|
||||||
|
|
||||||
const ToastRenderer = () => {
|
const ToastRenderer = () => {
|
||||||
// @ts-ignore-next-line
|
// @ts-expect-error
|
||||||
const { toastData, setToast } = useContext(UIContext);
|
const { toastData, setToast } = useContext(UIContext);
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|||||||
@ -118,12 +118,11 @@ export const modalStyles = {
|
|||||||
export const updateIndexInArray = (array, index, newValue) =>
|
export const updateIndexInArray = (array, index, newValue) =>
|
||||||
array.map((v, i) => (i === index ? newValue : v));
|
array.map((v, i) => (i === index ? newValue : v));
|
||||||
|
|
||||||
export const showPnpsFeedback = user => {
|
export const showPnpsFeedback = (feedbackList) => {
|
||||||
if (!user) return;
|
if (!feedbackList) return;
|
||||||
if (!user.feedback) return;
|
if (feedbackList.length > 0) {
|
||||||
if (user.feedback.length > 0) {
|
const feedback = feedbackList.find(
|
||||||
const feedback = user.feedback.find(
|
feedback => feedback.feedbackId === PNPS_FEEDBACK_ID
|
||||||
feedback => feedback.feedbackId === 'pnps'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!feedback) return false;
|
if (!feedback) return false;
|
||||||
@ -143,3 +142,5 @@ export const showPnpsFeedback = user => {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PNPS_FEEDBACK_ID = 'pnps'
|
||||||
|
|||||||
@ -54,7 +54,6 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
},
|
},
|
||||||
errorMessage: {
|
errorMessage: {
|
||||||
//@ts-ignore
|
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import { ADMIN } from '../../providers/AccessProvider/permissions';
|
|||||||
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
|
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
|
||||||
|
|
||||||
const CreateEnvironment = () => {
|
const CreateEnvironment = () => {
|
||||||
/* @ts-ignore */
|
|
||||||
const { setToastApiError, setToastData } = useToast();
|
const { setToastApiError, setToastData } = useToast();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|||||||
@ -38,7 +38,6 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
},
|
},
|
||||||
errorMessage: {
|
errorMessage: {
|
||||||
//@ts-ignore
|
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@ -6,7 +6,6 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
},
|
},
|
||||||
formHeader: {
|
formHeader: {
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
//@ts-ignore
|
|
||||||
fontSize: theme.fontSizes.bodySize,
|
fontSize: theme.fontSizes.bodySize,
|
||||||
marginTop: '1.5rem',
|
marginTop: '1.5rem',
|
||||||
marginBottom: '0.5rem',
|
marginBottom: '0.5rem',
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import { useContext } from 'react';
|
|||||||
import UIContext from '../../../contexts/UIContext';
|
import UIContext from '../../../contexts/UIContext';
|
||||||
|
|
||||||
const CreateFeature = () => {
|
const CreateFeature = () => {
|
||||||
/* @ts-ignore */
|
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { setShowFeedback } = useContext(UIContext);
|
const { setShowFeedback } = useContext(UIContext);
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import PermissionButton from '../../common/PermissionButton/PermissionButton';
|
|||||||
import { UPDATE_FEATURE } from '../../providers/AccessProvider/permissions';
|
import { UPDATE_FEATURE } from '../../providers/AccessProvider/permissions';
|
||||||
|
|
||||||
const EditFeature = () => {
|
const EditFeature = () => {
|
||||||
/* @ts-ignore */
|
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|||||||
@ -38,7 +38,6 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
marginBottom: '0.5rem',
|
marginBottom: '0.5rem',
|
||||||
},
|
},
|
||||||
typeDescription: {
|
typeDescription: {
|
||||||
//@ts-ignore
|
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
color: theme.palette.grey[600],
|
color: theme.palette.grey[600],
|
||||||
top: '-13px',
|
top: '-13px',
|
||||||
@ -55,7 +54,6 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
},
|
},
|
||||||
errorMessage: {
|
errorMessage: {
|
||||||
//@ts-ignore
|
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@ -24,7 +24,7 @@ interface IFeatureToggleListNewProps {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
//@ts-ignore
|
// @ts-expect-error
|
||||||
const sortList = (list, sortOpt) => {
|
const sortList = (list, sortOpt) => {
|
||||||
if (!list) {
|
if (!list) {
|
||||||
return list;
|
return list;
|
||||||
@ -33,7 +33,7 @@ const sortList = (list, sortOpt) => {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
if (sortOpt.type === 'string') {
|
if (sortOpt.type === 'string') {
|
||||||
//@ts-ignore
|
// @ts-expect-error
|
||||||
return list.sort((a, b) => {
|
return list.sort((a, b) => {
|
||||||
const fieldA = a[sortOpt.field]?.toUpperCase();
|
const fieldA = a[sortOpt.field]?.toUpperCase();
|
||||||
const fieldB = b[sortOpt.field]?.toUpperCase();
|
const fieldB = b[sortOpt.field]?.toUpperCase();
|
||||||
@ -49,7 +49,7 @@ const sortList = (list, sortOpt) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (sortOpt.type === 'date') {
|
if (sortOpt.type === 'date') {
|
||||||
//@ts-ignore
|
// @ts-expect-error
|
||||||
return list.sort((a, b) => {
|
return list.sort((a, b) => {
|
||||||
const fieldA = new Date(a[sortOpt.field]);
|
const fieldA = new Date(a[sortOpt.field]);
|
||||||
const fieldB = new Date(b[sortOpt.field]);
|
const fieldB = new Date(b[sortOpt.field]);
|
||||||
|
|||||||
@ -75,7 +75,6 @@ const FeatureStatus = ({
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!!lastSeenAt}
|
condition={!!lastSeenAt}
|
||||||
show={
|
show={
|
||||||
//@ts-ignore
|
|
||||||
<TimeAgo
|
<TimeAgo
|
||||||
date={lastSeenAt}
|
date={lastSeenAt}
|
||||||
title=""
|
title=""
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { Info } from '@material-ui/icons';
|
|||||||
|
|
||||||
import { weightTypes } from './enums';
|
import { weightTypes } from './enums';
|
||||||
|
|
||||||
import OverrideConfig from './OverrideConfig/OverrideConfig';
|
import { OverrideConfig } from './OverrideConfig/OverrideConfig';
|
||||||
import ConditionallyRender from '../../../../../common/ConditionallyRender';
|
import ConditionallyRender from '../../../../../common/ConditionallyRender';
|
||||||
import GeneralSelect from '../../../../../common/GeneralSelect/GeneralSelect';
|
import GeneralSelect from '../../../../../common/GeneralSelect/GeneralSelect';
|
||||||
import { useCommonStyles } from '../../../../../../common.styles';
|
import { useCommonStyles } from '../../../../../../common.styles';
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Grid, IconButton, TextField } from '@material-ui/core';
|
import { Grid, IconButton, TextField } from '@material-ui/core';
|
||||||
import { Delete } from '@material-ui/icons';
|
import { Delete } from '@material-ui/icons';
|
||||||
@ -10,17 +8,19 @@ import GeneralSelect from '../../../../../../common/GeneralSelect/GeneralSelect'
|
|||||||
import { useCommonStyles } from '../../../../../../../common.styles';
|
import { useCommonStyles } from '../../../../../../../common.styles';
|
||||||
import ConditionallyRender from '../../../../../../common/ConditionallyRender';
|
import ConditionallyRender from '../../../../../../common/ConditionallyRender';
|
||||||
import InputListField from '../../../../../../common/input-list-field.jsx';
|
import InputListField from '../../../../../../common/input-list-field.jsx';
|
||||||
|
import useUnleashContext from '../../../../../../../hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||||
|
|
||||||
const OverrideConfig = ({
|
export const OverrideConfig = ({
|
||||||
overrides,
|
overrides,
|
||||||
updateOverrideType,
|
updateOverrideType,
|
||||||
updateOverrideValues,
|
updateOverrideValues,
|
||||||
removeOverride,
|
removeOverride,
|
||||||
contextDefinitions,
|
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
const contextNames = contextDefinitions.map(c => ({
|
|
||||||
|
const { context } = useUnleashContext();
|
||||||
|
const contextNames = context.map(c => ({
|
||||||
key: c.name,
|
key: c.name,
|
||||||
label: c.name,
|
label: c.name,
|
||||||
}));
|
}));
|
||||||
@ -34,9 +34,7 @@ const OverrideConfig = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return overrides.map((o, i) => {
|
return overrides.map((o, i) => {
|
||||||
const definition = contextDefinitions.find(
|
const definition = context.find(c => c.name === o.contextName);
|
||||||
c => c.name === o.contextName
|
|
||||||
);
|
|
||||||
const legalValues = definition ? definition.legalValues : [];
|
const legalValues = definition ? definition.legalValues : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -115,9 +113,3 @@ OverrideConfig.propTypes = {
|
|||||||
updateOverrideValues: PropTypes.func.isRequired,
|
updateOverrideValues: PropTypes.func.isRequired,
|
||||||
removeOverride: PropTypes.func.isRequired,
|
removeOverride: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
contextDefinitions: state.context.toJS(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, {})(OverrideConfig);
|
|
||||||
|
|||||||
@ -147,7 +147,6 @@ const FeatureOverviewVariants = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await patchFeatureVariants(projectId, featureId, patch);
|
const res = await patchFeatureVariants(projectId, featureId, patch);
|
||||||
// @ts-ignore
|
|
||||||
const { variants } = await res.json();
|
const { variants } = await res.json();
|
||||||
mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false);
|
mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false);
|
||||||
setToastData({
|
setToastData({
|
||||||
@ -204,7 +203,6 @@ const FeatureOverviewVariants = () => {
|
|||||||
if (patch.length === 0) return;
|
if (patch.length === 0) return;
|
||||||
try {
|
try {
|
||||||
const res = await patchFeatureVariants(projectId, featureId, patch);
|
const res = await patchFeatureVariants(projectId, featureId, patch);
|
||||||
// @ts-ignore
|
|
||||||
const { variants } = await res.json();
|
const { variants } = await res.json();
|
||||||
mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false);
|
mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false);
|
||||||
setToastData({
|
setToastData({
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
import { matchPath } from 'react-router';
|
import { matchPath } from 'react-router';
|
||||||
import MainLayout from '../MainLayout';
|
import { MainLayout } from '../MainLayout/MainLayout';
|
||||||
|
|
||||||
const LayoutPicker = ({ children, location }) => {
|
const LayoutPicker = ({ children, location }) => {
|
||||||
const standalonePages = () => {
|
const standalonePages = () => {
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { makeStyles } from '@material-ui/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import { Grid } from '@material-ui/core';
|
import { Grid } from '@material-ui/core';
|
||||||
|
|
||||||
import styles from '../../styles.module.scss';
|
import styles from '../../styles.module.scss';
|
||||||
import ErrorContainer from '../../error/error-container';
|
import ErrorContainer from '../../error/error-container';
|
||||||
import Header from '../../menu/Header/Header';
|
import Header from '../../menu/Header/Header';
|
||||||
@ -11,6 +9,7 @@ import Footer from '../../menu/Footer/Footer';
|
|||||||
import Proclamation from '../../common/Proclamation/Proclamation';
|
import Proclamation from '../../common/Proclamation/Proclamation';
|
||||||
import BreadcrumbNav from '../../common/BreadcrumbNav/BreadcrumbNav';
|
import BreadcrumbNav from '../../common/BreadcrumbNav/BreadcrumbNav';
|
||||||
import { ReactComponent as Texture } from '../../../assets/img/texture.svg';
|
import { ReactComponent as Texture } from '../../../assets/img/texture.svg';
|
||||||
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles(theme => ({
|
||||||
container: {
|
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 muiStyles = useStyles();
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header location={location} />
|
<Header />
|
||||||
<Grid container className={muiStyles.container}>
|
<Grid container className={muiStyles.container}>
|
||||||
<div className={classnames(styles.contentWrapper)}>
|
<div className={classnames(styles.contentWrapper)}>
|
||||||
<Grid item className={styles.content} xs={12} sm={12}>
|
<Grid item className={styles.content} xs={12} sm={12}>
|
||||||
<div
|
<div
|
||||||
className={muiStyles.contentContainer}
|
className={muiStyles.contentContainer}
|
||||||
style={{ zIndex: '200' }}
|
style={{ zIndex: 200 }}
|
||||||
>
|
>
|
||||||
<BreadcrumbNav />
|
<BreadcrumbNav />
|
||||||
<Proclamation toast={uiConfig.toast} />
|
<Proclamation toast={uiConfig.toast} />
|
||||||
@ -52,7 +56,7 @@ const MainLayout = ({ children, location, uiConfig }) => {
|
|||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
right: '0',
|
right: '0',
|
||||||
bottom: '-4px',
|
bottom: '-4px',
|
||||||
zIndex: '1',
|
zIndex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Texture />
|
<Texture />
|
||||||
@ -64,9 +68,3 @@ const MainLayout = ({ children, location, uiConfig }) => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
MainLayout.propTypes = {
|
|
||||||
location: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MainLayout;
|
|
||||||
@ -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);
|
|
||||||
@ -7,7 +7,7 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
padding: '0.5rem',
|
padding: '0.5rem',
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
zIndex: '300',
|
zIndex: 300,
|
||||||
},
|
},
|
||||||
links: {
|
links: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@ -286,7 +286,7 @@ Array [
|
|||||||
"layout": "main",
|
"layout": "main",
|
||||||
"menu": Object {},
|
"menu": Object {},
|
||||||
"parent": "/addons",
|
"parent": "/addons",
|
||||||
"path": "/addons/create/:provider",
|
"path": "/addons/create/:providerId",
|
||||||
"title": "Create",
|
"title": "Create",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
@ -295,7 +295,7 @@ Array [
|
|||||||
"layout": "main",
|
"layout": "main",
|
||||||
"menu": Object {},
|
"menu": Object {},
|
||||||
"parent": "/addons",
|
"parent": "/addons",
|
||||||
"path": "/addons/edit/:id",
|
"path": "/addons/edit/:addonId",
|
||||||
"title": "Edit",
|
"title": "Edit",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -6,10 +6,8 @@ import Strategies from '../../page/strategies';
|
|||||||
import HistoryPage from '../../page/history';
|
import HistoryPage from '../../page/history';
|
||||||
import HistoryTogglePage from '../../page/history/toggle';
|
import HistoryTogglePage from '../../page/history/toggle';
|
||||||
import { ArchiveListContainer } from '../archive/ArchiveListContainer';
|
import { ArchiveListContainer } from '../archive/ArchiveListContainer';
|
||||||
import ListTagTypes from '../../page/tag-types';
|
import { TagTypeList } from '../tags/TagTypeList/TagTypeList';
|
||||||
import Addons from '../../page/addons';
|
import { AddonList } from '../addons/AddonList/AddonList';
|
||||||
import AddonsCreate from '../../page/addons/create';
|
|
||||||
import AddonsEdit from '../../page/addons/edit';
|
|
||||||
import Admin from '../admin';
|
import Admin from '../admin';
|
||||||
import AdminApi from '../admin/api';
|
import AdminApi from '../admin/api';
|
||||||
import AdminInvoice from '../admin/invoice/InvoiceAdminPage';
|
import AdminInvoice from '../admin/invoice/InvoiceAdminPage';
|
||||||
@ -35,8 +33,8 @@ import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironme
|
|||||||
import EditEnvironment from '../environments/EditEnvironment/EditEnvironment';
|
import EditEnvironment from '../environments/EditEnvironment/EditEnvironment';
|
||||||
import CreateContext from '../context/CreateContext/CreateContext';
|
import CreateContext from '../context/CreateContext/CreateContext';
|
||||||
import EditContext from '../context/EditContext/EditContext';
|
import EditContext from '../context/EditContext/EditContext';
|
||||||
import EditTagType from '../tagTypes/EditTagType/EditTagType';
|
import EditTagType from '../tags/EditTagType/EditTagType';
|
||||||
import CreateTagType from '../tagTypes/CreateTagType/CreateTagType';
|
import CreateTagType from '../tags/CreateTagType/CreateTagType';
|
||||||
import EditProject from '../project/Project/EditProject/EditProject';
|
import EditProject from '../project/Project/EditProject/EditProject';
|
||||||
import CreateProject from '../project/Project/CreateProject/CreateProject';
|
import CreateProject from '../project/Project/CreateProject/CreateProject';
|
||||||
import CreateFeature from '../feature/CreateFeature/CreateFeature';
|
import CreateFeature from '../feature/CreateFeature/CreateFeature';
|
||||||
@ -45,6 +43,8 @@ import { ApplicationEdit } from '../application/ApplicationEdit/ApplicationEdit'
|
|||||||
import { ApplicationList } from '../application/ApplicationList/ApplicationList';
|
import { ApplicationList } from '../application/ApplicationList/ApplicationList';
|
||||||
import ContextList from '../context/ContextList/ContextList';
|
import ContextList from '../context/ContextList/ContextList';
|
||||||
import RedirectFeatureView from '../feature/RedirectFeatureView/RedirectFeatureView';
|
import RedirectFeatureView from '../feature/RedirectFeatureView/RedirectFeatureView';
|
||||||
|
import { CreateAddon } from '../addons/CreateAddon/CreateAddon';
|
||||||
|
import { EditAddon } from '../addons/EditAddon/EditAddon';
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
// Project
|
// Project
|
||||||
@ -314,7 +314,7 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/tag-types',
|
path: '/tag-types',
|
||||||
title: 'Tag types',
|
title: 'Tag types',
|
||||||
component: ListTagTypes,
|
component: TagTypeList,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: { mobile: true, advanced: true },
|
menu: { mobile: true, advanced: true },
|
||||||
@ -322,19 +322,19 @@ export const routes = [
|
|||||||
|
|
||||||
// Addons
|
// Addons
|
||||||
{
|
{
|
||||||
path: '/addons/create/:provider',
|
path: '/addons/create/:providerId',
|
||||||
parent: '/addons',
|
parent: '/addons',
|
||||||
title: 'Create',
|
title: 'Create',
|
||||||
component: AddonsCreate,
|
component: CreateAddon,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: {},
|
menu: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/addons/edit/:id',
|
path: '/addons/edit/:addonId',
|
||||||
parent: '/addons',
|
parent: '/addons',
|
||||||
title: 'Edit',
|
title: 'Edit',
|
||||||
component: AddonsEdit,
|
component: EditAddon,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: {},
|
menu: {},
|
||||||
@ -342,7 +342,7 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/addons',
|
path: '/addons',
|
||||||
title: 'Addons',
|
title: 'Addons',
|
||||||
component: Addons,
|
component: AddonList,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import PermissionButton from '../../../common/PermissionButton/PermissionButton'
|
|||||||
import { CREATE_PROJECT } from '../../../providers/AccessProvider/permissions';
|
import { CREATE_PROJECT } from '../../../providers/AccessProvider/permissions';
|
||||||
|
|
||||||
const CreateProject = () => {
|
const CreateProject = () => {
|
||||||
/* @ts-ignore */
|
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { refetch } = useUser();
|
const { refetch } = useUser();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import useQueryParams from '../../../hooks/useQueryParams';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import useTabs from '../../../hooks/useTabs';
|
import useTabs from '../../../hooks/useTabs';
|
||||||
import TabPanel from '../../common/TabNav/TabPanel';
|
import TabPanel from '../../common/TabNav/TabPanel';
|
||||||
import ProjectAccess from '../access-container';
|
import { ProjectAccess } from '../ProjectAccess/ProjectAccess';
|
||||||
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment';
|
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment';
|
||||||
import ProjectOverview from './ProjectOverview';
|
import ProjectOverview from './ProjectOverview';
|
||||||
import ProjectHealth from './ProjectHealth/ProjectHealth';
|
import ProjectHealth from './ProjectHealth/ProjectHealth';
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
},
|
},
|
||||||
errorMessage: {
|
errorMessage: {
|
||||||
//@ts-ignore
|
// @ts-expect-error
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@ -11,18 +11,11 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
backgroundColor: '#efefef',
|
backgroundColor: '#efefef',
|
||||||
marginTop: '2rem',
|
marginTop: '2rem',
|
||||||
},
|
},
|
||||||
actionList: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
inputLabel: { backgroundColor: '#fff' },
|
inputLabel: { backgroundColor: '#fff' },
|
||||||
roleName: {
|
roleName: {
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
padding: '5px 0px',
|
padding: '5px 0px',
|
||||||
},
|
},
|
||||||
iconButton: {
|
|
||||||
marginLeft: '0.5rem',
|
|
||||||
},
|
|
||||||
menuItem: {
|
menuItem: {
|
||||||
width: '340px',
|
width: '340px',
|
||||||
whiteSpace: 'normal',
|
whiteSpace: 'normal',
|
||||||
|
|||||||
@ -1,71 +1,40 @@
|
|||||||
/* eslint-disable react/jsx-no-target-blank */
|
/* eslint-disable react/jsx-no-target-blank */
|
||||||
import { useEffect, useState } from 'react';
|
import React, { 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 { Alert } from '@material-ui/lab';
|
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 PageContent from '../../common/PageContent';
|
||||||
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { useStyles } from './ProjectAccess.styles';
|
import { useStyles } from './ProjectAccess.styles';
|
||||||
import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton';
|
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { IFeatureViewParams } from '../../../interfaces/params';
|
import { IProjectViewParams } from '../../../interfaces/params';
|
||||||
import ProjectRoleSelect from './ProjectRoleSelect/ProjectRoleSelect';
|
|
||||||
import usePagination from '../../../hooks/usePagination';
|
import usePagination from '../../../hooks/usePagination';
|
||||||
import PaginateUI from '../../common/PaginateUI/PaginateUI';
|
import PaginateUI from '../../common/PaginateUI/PaginateUI';
|
||||||
import useToast from '../../../hooks/useToast';
|
import useToast from '../../../hooks/useToast';
|
||||||
import ConfirmDialogue from '../../common/Dialogue';
|
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 = () => {
|
export const ProjectAccess = () => {
|
||||||
const { id } = useParams<IFeatureViewParams>();
|
const { id: projectId } = useParams<IProjectViewParams>();
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const [roles, setRoles] = useState([]);
|
const { access, refetchProjectAccess } = useProjectAccess(projectId);
|
||||||
const [users, setUsers] = useState([]);
|
const { setToastData } = useToast();
|
||||||
const [error, setError] = useState();
|
|
||||||
const { setToastData, setToastApiError } = useToast();
|
|
||||||
const { isOss } = useUiConfig();
|
const { isOss } = useUiConfig();
|
||||||
const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } =
|
const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } =
|
||||||
usePagination(users, 10);
|
usePagination(access.users, 10);
|
||||||
|
const { removeUserFromRole, addUserToRole } = useProjectApi();
|
||||||
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
||||||
const [user, setUser] = useState({});
|
const [user, setUser] = useState<IProjectAccessUser | undefined>();
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isOss()) {
|
if (isOss()) {
|
||||||
return (
|
return (
|
||||||
<PageContent>
|
<PageContent headerContent={<HeaderTitle title="Project Access" />}>
|
||||||
<Alert severity="error">
|
<Alert severity="error">
|
||||||
Controlling access to projects requires a paid version of
|
Controlling access to projects requires a paid version of
|
||||||
Unleash. Check out{' '}
|
Unleash. Check out{' '}
|
||||||
@ -78,23 +47,25 @@ const ProjectAccess = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRoleChange = (userId, currRoleId) => async evt => {
|
const handleRoleChange =
|
||||||
const roleId = evt.target.value;
|
(userId: number, currRoleId: number) =>
|
||||||
|
async (
|
||||||
|
evt: React.ChangeEvent<{
|
||||||
|
name?: string;
|
||||||
|
value: unknown;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const roleId = Number(evt.target.value);
|
||||||
try {
|
try {
|
||||||
await projectApi.removeUserFromRole(id, currRoleId, userId);
|
await removeUserFromRole(projectId, currRoleId, userId);
|
||||||
await projectApi.addUserToRole(id, roleId, userId).then(() => {
|
await addUserToRole(projectId, roleId, userId);
|
||||||
|
refetchProjectAccess();
|
||||||
|
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'User role changed successfully',
|
title: 'User role changed successfully',
|
||||||
});
|
});
|
||||||
});
|
} catch (err: any) {
|
||||||
const newUsers = users.map(u => {
|
|
||||||
if (u.id === userId) {
|
|
||||||
return { ...u, roleId };
|
|
||||||
} else return u;
|
|
||||||
});
|
|
||||||
setUsers(newUsers);
|
|
||||||
} catch (err) {
|
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: err.message || 'Server problems when adding users.',
|
title: err.message || 'Server problems when adding users.',
|
||||||
@ -102,34 +73,23 @@ const ProjectAccess = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addUser = async (userId, roleId) => {
|
const handleRemoveAccess = (user: IProjectAccessUser) => {
|
||||||
try {
|
setUser(user);
|
||||||
await projectApi.addUserToRole(id, roleId, userId);
|
setShowDelDialogue(true);
|
||||||
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 = (userId: number, roleId: number) => async () => {
|
const removeAccess = (user: IProjectAccessUser | undefined) => async () => {
|
||||||
|
if (!user) return;
|
||||||
|
const { id, roleId } = user;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await projectApi.removeUserFromRole(id, roleId, userId).then(() => {
|
await removeUserFromRole(projectId, roleId, id);
|
||||||
|
refetchProjectAccess();
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'User have been removed from project',
|
title: 'The user has been removed from project',
|
||||||
});
|
});
|
||||||
});
|
} catch (err: any) {
|
||||||
const newUsers = users.filter(u => u.id !== userId);
|
|
||||||
setUsers(newUsers);
|
|
||||||
} catch (err) {
|
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: err.message || 'Server problems when adding users.',
|
title: err.message || 'Server problems when adding users.',
|
||||||
@ -138,91 +98,20 @@ const ProjectAccess = () => {
|
|||||||
setShowDelDialogue(false);
|
setShowDelDialogue(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseError = () => {
|
|
||||||
setError(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent className={styles.pageContent}>
|
<PageContent
|
||||||
<AddUserComponent roles={roles} addUserToRole={addUser} />
|
headerContent={<HeaderTitle title="Project Roles"></HeaderTitle>}
|
||||||
<Dialog
|
className={styles.pageContent}
|
||||||
open={!!error}
|
|
||||||
onClose={handleCloseError}
|
|
||||||
aria-labelledby="alert-dialog-title"
|
|
||||||
aria-describedby="alert-dialog-description"
|
|
||||||
>
|
>
|
||||||
<DialogTitle id="alert-dialog-title">{'Error'}</DialogTitle>
|
<ProjectAccessAddUser roles={access?.roles} />
|
||||||
<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>
|
<div className={styles.divider}></div>
|
||||||
<List>
|
<ProjectAccessList
|
||||||
{page.map(user => {
|
handleRoleChange={handleRoleChange}
|
||||||
const labelId = `checkbox-list-secondary-label-${user.id}`;
|
handleRemoveAccess={handleRemoveAccess}
|
||||||
return (
|
page={page}
|
||||||
<ListItem key={user.id} button>
|
access={access}
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<PaginateUI
|
<PaginateUI
|
||||||
pages={pages}
|
pages={pages}
|
||||||
pageIndex={pageIndex}
|
pageIndex={pageIndex}
|
||||||
@ -231,12 +120,13 @@ const ProjectAccess = () => {
|
|||||||
prevPage={prevPage}
|
prevPage={prevPage}
|
||||||
style={{ bottom: '-21px' }}
|
style={{ bottom: '-21px' }}
|
||||||
/>
|
/>
|
||||||
</List>
|
</ProjectAccessList>
|
||||||
|
|
||||||
<ConfirmDialogue
|
<ConfirmDialogue
|
||||||
open={showDelDialogue}
|
open={showDelDialogue}
|
||||||
onClick={removeAccess(user.id, user.roleId)}
|
onClick={removeAccess(user)}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setUser({});
|
setUser(undefined);
|
||||||
setShowDelDialogue(false);
|
setShowDelDialogue(false);
|
||||||
}}
|
}}
|
||||||
title="Really remove user from this project"
|
title="Really remove user from this project"
|
||||||
@ -244,5 +134,3 @@ const ProjectAccess = () => {
|
|||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectAccess;
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(() => ({
|
||||||
|
iconButton: {
|
||||||
|
marginLeft: '0.5rem',
|
||||||
|
},
|
||||||
|
actionList: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
}));
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,19 +1,24 @@
|
|||||||
import { FormControl, InputLabel, Select, MenuItem } from '@material-ui/core';
|
import { FormControl, InputLabel, Select, MenuItem } from '@material-ui/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import IRole from '../../../../interfaces/role';
|
import { IProjectRole } from '../../../../interfaces/role';
|
||||||
|
|
||||||
import { useStyles } from '../ProjectAccess.styles';
|
import { useStyles } from '../ProjectAccess.styles';
|
||||||
|
|
||||||
interface IProjectRoleSelect {
|
interface IProjectRoleSelect {
|
||||||
roles: IRole[];
|
roles: IProjectRole[];
|
||||||
labelId: string;
|
labelId: string;
|
||||||
id: string;
|
id: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
onChange: () => void;
|
onChange: (
|
||||||
|
evt: React.ChangeEvent<{
|
||||||
|
name?: string | undefined;
|
||||||
|
value: unknown;
|
||||||
|
}>
|
||||||
|
) => void;
|
||||||
value: any;
|
value: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
|
export const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
|
||||||
roles,
|
roles,
|
||||||
onChange,
|
onChange,
|
||||||
labelId,
|
labelId,
|
||||||
@ -39,9 +44,10 @@ const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
|
|||||||
value={value || ''}
|
value={value || ''}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
renderValue={roleId => {
|
renderValue={roleId => {
|
||||||
return roles?.find(role => {
|
const role = roles?.find(role => {
|
||||||
return role.id === roleId;
|
return role.id === roleId;
|
||||||
}).name;
|
});
|
||||||
|
return role?.name || '';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@ -66,5 +72,3 @@ const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectRoleSelect;
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import TagTypeList from './TagTypeList';
|
|
||||||
|
|
||||||
export default TagTypeList;
|
|
||||||
@ -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>
|
|
||||||
`;
|
|
||||||
@ -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();
|
|
||||||
});
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -5,7 +5,7 @@ import useToast from '../../../hooks/useToast';
|
|||||||
import FormTemplate from '../../common/FormTemplate/FormTemplate';
|
import FormTemplate from '../../common/FormTemplate/FormTemplate';
|
||||||
import PermissionButton from '../../common/PermissionButton/PermissionButton';
|
import PermissionButton from '../../common/PermissionButton/PermissionButton';
|
||||||
import { UPDATE_TAG_TYPE } from '../../providers/AccessProvider/permissions';
|
import { UPDATE_TAG_TYPE } from '../../providers/AccessProvider/permissions';
|
||||||
import useTagForm from '../hooks/useTagForm';
|
import useTagTypeForm from '../TagTypeForm/useTagTypeForm';
|
||||||
import TagTypeForm from '../TagTypeForm/TagTypeForm';
|
import TagTypeForm from '../TagTypeForm/TagTypeForm';
|
||||||
|
|
||||||
const CreateTagType = () => {
|
const CreateTagType = () => {
|
||||||
@ -21,7 +21,7 @@ const CreateTagType = () => {
|
|||||||
validateNameUniqueness,
|
validateNameUniqueness,
|
||||||
errors,
|
errors,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
} = useTagForm();
|
} = useTagTypeForm();
|
||||||
const { createTag, loading } = useTagTypesApi();
|
const { createTag, loading } = useTagTypesApi();
|
||||||
|
|
||||||
const handleSubmit = async (e: Event) => {
|
const handleSubmit = async (e: Event) => {
|
||||||
@ -6,7 +6,7 @@ import useToast from '../../../hooks/useToast';
|
|||||||
import FormTemplate from '../../common/FormTemplate/FormTemplate';
|
import FormTemplate from '../../common/FormTemplate/FormTemplate';
|
||||||
import PermissionButton from '../../common/PermissionButton/PermissionButton';
|
import PermissionButton from '../../common/PermissionButton/PermissionButton';
|
||||||
import { UPDATE_TAG_TYPE } from '../../providers/AccessProvider/permissions';
|
import { UPDATE_TAG_TYPE } from '../../providers/AccessProvider/permissions';
|
||||||
import useTagForm from '../hooks/useTagForm';
|
import useTagTypeForm from '../TagTypeForm/useTagTypeForm';
|
||||||
import TagForm from '../TagTypeForm/TagTypeForm';
|
import TagForm from '../TagTypeForm/TagTypeForm';
|
||||||
|
|
||||||
const EditTagType = () => {
|
const EditTagType = () => {
|
||||||
@ -23,7 +23,7 @@ const EditTagType = () => {
|
|||||||
getTagPayload,
|
getTagPayload,
|
||||||
errors,
|
errors,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
} = useTagForm(tagType?.name, tagType?.description);
|
} = useTagTypeForm(tagType?.name, tagType?.description);
|
||||||
const { updateTagType, loading } = useTagTypesApi();
|
const { updateTagType, loading } = useTagTypesApi();
|
||||||
|
|
||||||
const handleSubmit = async (e: Event) => {
|
const handleSubmit = async (e: Event) => {
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { makeStyles } from '@material-ui/styles';
|
|
||||||
|
|
||||||
export const useStyles = makeStyles({
|
|
||||||
tagListItem: {
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import TagsListComponent from './TagList';
|
|
||||||
|
|
||||||
export default TagsListComponent;
|
|
||||||
@ -38,7 +38,6 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
},
|
},
|
||||||
errorMessage: {
|
errorMessage: {
|
||||||
//@ts-ignore
|
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi';
|
import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi';
|
||||||
|
|
||||||
const useTagForm = (initialTagName = '', initialTagDesc = '') => {
|
const useTagTypeForm = (initialTagName = '', initialTagDesc = '') => {
|
||||||
const [tagName, setTagName] = useState(initialTagName);
|
const [tagName, setTagName] = useState(initialTagName);
|
||||||
const [tagDesc, setTagDesc] = useState(initialTagDesc);
|
const [tagDesc, setTagDesc] = useState(initialTagDesc);
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
@ -66,4 +66,4 @@ const useTagForm = (initialTagName = '', initialTagDesc = '') => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useTagForm;
|
export default useTagTypeForm;
|
||||||
@ -2,12 +2,12 @@ import { useContext, useState } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
IconButton,
|
|
||||||
Button,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import { Add, Delete, Edit, Label } from '@material-ui/icons';
|
import { Add, Delete, Edit, Label } from '@material-ui/icons';
|
||||||
@ -20,14 +20,14 @@ import {
|
|||||||
} from '../../providers/AccessProvider/permissions';
|
} from '../../providers/AccessProvider/permissions';
|
||||||
import Dialogue from '../../common/Dialogue/Dialogue';
|
import Dialogue from '../../common/Dialogue/Dialogue';
|
||||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
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 AccessContext from '../../../contexts/AccessContext';
|
||||||
import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi';
|
import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi';
|
||||||
import useTagTypes from '../../../hooks/api/getters/useTagTypes/useTagTypes';
|
import useTagTypes from '../../../hooks/api/getters/useTagTypes/useTagTypes';
|
||||||
import useToast from '../../../hooks/useToast';
|
import useToast from '../../../hooks/useToast';
|
||||||
import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton';
|
import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton';
|
||||||
|
|
||||||
const TagTypeList = () => {
|
export const TagTypeList = () => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const [deletion, setDeletion] = useState({ open: false });
|
const [deletion, setDeletion] = useState({ open: false });
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@ -160,5 +160,3 @@ TagTypeList.propTypes = {
|
|||||||
fetchTagTypes: PropTypes.func.isRequired,
|
fetchTagTypes: PropTypes.func.isRequired,
|
||||||
removeTagType: PropTypes.func.isRequired,
|
removeTagType: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TagTypeList;
|
|
||||||
@ -1,20 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { TagTypeList } from '../TagTypeList';
|
||||||
import TagTypesList from '../TagTypeList';
|
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { ThemeProvider } from '@material-ui/styles';
|
import { ThemeProvider } from '@material-ui/styles';
|
||||||
import theme from '../../../themes/main-theme';
|
import theme from '../../../../themes/main-theme';
|
||||||
import { createFakeStore } from '../../../accessStoreFake';
|
import { createFakeStore } from '../../../../accessStoreFake';
|
||||||
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
|
import AccessProvider from '../../../providers/AccessProvider/AccessProvider';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ADMIN,
|
ADMIN,
|
||||||
CREATE_TAG_TYPE,
|
CREATE_TAG_TYPE,
|
||||||
UPDATE_TAG_TYPE,
|
UPDATE_TAG_TYPE,
|
||||||
DELETE_TAG_TYPE,
|
DELETE_TAG_TYPE,
|
||||||
} from '../../providers/AccessProvider/permissions';
|
} from '../../../providers/AccessProvider/permissions';
|
||||||
import UIProvider from '../../providers/UIProvider/UIProvider';
|
import UIProvider from '../../../providers/UIProvider/UIProvider';
|
||||||
|
|
||||||
test('renders an empty list correctly', () => {
|
test('renders an empty list correctly', () => {
|
||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
@ -24,7 +22,7 @@ test('renders an empty list correctly', () => {
|
|||||||
<AccessProvider
|
<AccessProvider
|
||||||
store={createFakeStore([{ permission: ADMIN }])}
|
store={createFakeStore([{ permission: ADMIN }])}
|
||||||
>
|
>
|
||||||
<TagTypesList
|
<TagTypeList
|
||||||
tagTypes={[]}
|
tagTypes={[]}
|
||||||
fetchTagTypes={jest.fn()}
|
fetchTagTypes={jest.fn()}
|
||||||
removeTagType={jest.fn()}
|
removeTagType={jest.fn()}
|
||||||
@ -50,7 +48,7 @@ test('renders a list with elements correctly', () => {
|
|||||||
{ permission: DELETE_TAG_TYPE },
|
{ permission: DELETE_TAG_TYPE },
|
||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
<TagTypesList
|
<TagTypeList
|
||||||
tagTypes={[
|
tagTypes={[
|
||||||
{
|
{
|
||||||
name: 'simple',
|
name: 'simple',
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -3,7 +3,7 @@ import { makeStyles } from '@material-ui/styles';
|
|||||||
export const useStyles = makeStyles(theme => ({
|
export const useStyles = makeStyles(theme => ({
|
||||||
profile: {
|
profile: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
zIndex: '5000',
|
zIndex: 5000,
|
||||||
minWidth: '300px',
|
minWidth: '300px',
|
||||||
right: 0,
|
right: 0,
|
||||||
padding: '1.5rem',
|
padding: '1.5rem',
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { IAddons } from '../../../../interfaces/addons';
|
import { IAddon } from '../../../../interfaces/addons';
|
||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
const useAddonsApi = () => {
|
const useAddonsApi = () => {
|
||||||
@ -8,7 +8,7 @@ const useAddonsApi = () => {
|
|||||||
|
|
||||||
const URI = 'api/admin/addons';
|
const URI = 'api/admin/addons';
|
||||||
|
|
||||||
const createAddon = async (addonConfig: IAddons) => {
|
const createAddon = async (addonConfig: IAddon) => {
|
||||||
const path = URI;
|
const path = URI;
|
||||||
const req = createRequest(path, {
|
const req = createRequest(path, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -38,7 +38,7 @@ const useAddonsApi = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateAddon = async (addonConfig: IAddons) => {
|
const updateAddon = async (addonConfig: IAddon) => {
|
||||||
const path = `${URI}/${addonConfig.id}`;
|
const path = `${URI}/${addonConfig.id}`;
|
||||||
const req = createRequest(path, {
|
const req = createRequest(path, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|||||||
@ -163,14 +163,19 @@ const useAPI = ({
|
|||||||
|
|
||||||
if (res.status > 399) {
|
if (res.status > 399) {
|
||||||
const response = await res.json();
|
const response = await res.json();
|
||||||
|
if (response?.details?.length > 0 && propagateErrors) {
|
||||||
if (response?.details?.length > 0) {
|
|
||||||
const error = response.details[0];
|
const error = response.details[0];
|
||||||
if (propagateErrors) {
|
if (propagateErrors) {
|
||||||
throw new Error(error.message);
|
throw new Error(error.message || error.msg);
|
||||||
}
|
}
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response?.length > 0 && propagateErrors) {
|
||||||
|
const error = response[0];
|
||||||
|
throw new Error(error.message || error.msg);
|
||||||
|
}
|
||||||
|
|
||||||
if (propagateErrors) {
|
if (propagateErrors) {
|
||||||
throw new Error('Action could not be performed');
|
throw new Error('Action could not be performed');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
return {
|
||||||
createProject,
|
createProject,
|
||||||
validateId,
|
validateId,
|
||||||
@ -114,8 +162,11 @@ const useProjectApi = () => {
|
|||||||
deleteProject,
|
deleteProject,
|
||||||
addEnvironmentToProject,
|
addEnvironmentToProject,
|
||||||
removeEnvironmentFromProject,
|
removeEnvironmentFromProject,
|
||||||
|
addUserToRole,
|
||||||
|
removeUserFromRole,
|
||||||
errors,
|
errors,
|
||||||
loading,
|
loading,
|
||||||
|
searchProjectUser,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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 to resolve body, but don't rethrow res.json is not a function
|
||||||
try {
|
try {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
error.info = await res.json();
|
error.info = await res.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
error.info = {};
|
error.info = {};
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
error.status = res.status;
|
error.status = res.status;
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
error.statusText = res.statusText;
|
error.statusText = res.statusText;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
@ -19,7 +19,7 @@ const usePagination = (
|
|||||||
const result = paginate(dataToPaginate, limit);
|
const result = paginate(dataToPaginate, limit);
|
||||||
setPaginatedData(result);
|
setPaginatedData(result);
|
||||||
/* eslint-disable-next-line */
|
/* eslint-disable-next-line */
|
||||||
}, [data, limit]);
|
}, [JSON.stringify(data), limit]);
|
||||||
|
|
||||||
const nextPage = () => {
|
const nextPage = () => {
|
||||||
if (pageIndex < paginatedData.length - 1) {
|
if (pageIndex < paginatedData.length - 1) {
|
||||||
|
|||||||
@ -20,7 +20,7 @@ interface IToastOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useToast = () => {
|
const useToast = () => {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
const { setToast } = useContext(UIContext);
|
const { setToast } = useContext(UIContext);
|
||||||
|
|
||||||
const hideToast = () =>
|
const hideToast = () =>
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
export interface IAddons {
|
import { ITagType } from './tags';
|
||||||
|
|
||||||
|
export interface IAddon {
|
||||||
id: number;
|
id: number;
|
||||||
provider: string;
|
provider: string;
|
||||||
description: string;
|
description: string;
|
||||||
@ -6,3 +8,32 @@ export interface IAddons {
|
|||||||
events: string[];
|
events: string[];
|
||||||
parameters: object;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -3,3 +3,7 @@ export interface IFeatureViewParams {
|
|||||||
featureId: string;
|
featureId: string;
|
||||||
activeTab: string;
|
activeTab: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IProjectViewParams {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export interface IProjectRole {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IRole;
|
export default IRole;
|
||||||
|
|||||||
@ -9,6 +9,14 @@ export interface IUiConfig {
|
|||||||
versionInfo: IVersionInfo;
|
versionInfo: IVersionInfo;
|
||||||
links: ILinks[];
|
links: ILinks[];
|
||||||
disablePasswordAuth?: boolean;
|
disablePasswordAuth?: boolean;
|
||||||
|
toast?: IProclamationToast
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProclamationToast {
|
||||||
|
message: string;
|
||||||
|
id: string;
|
||||||
|
severity: 'success' | 'info' | 'warning' | 'error';
|
||||||
|
link: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFlags {
|
export interface IFlags {
|
||||||
|
|||||||
@ -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
Loading…
Reference in New Issue
Block a user