diff --git a/frontend/package.json b/frontend/package.json index f44dd856b2..88a7544a8c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -67,6 +67,7 @@ "fetch-mock": "9.11.0", "http-proxy-middleware": "2.0.2", "immutable": "4.0.0", + "@types/lodash.clonedeep": "^4.5.6", "lodash.clonedeep": "4.5.0", "lodash.flow": "3.5.0", "node-fetch": "2.6.7", diff --git a/frontend/src/component/App.tsx b/frontend/src/component/App.tsx index e74f048560..e1a1fcef85 100644 --- a/frontend/src/component/App.tsx +++ b/frontend/src/component/App.tsx @@ -23,7 +23,6 @@ import ToastRenderer from './common/ToastRenderer/ToastRenderer'; interface IAppProps extends RouteComponentProps { user: IAuthStatus; fetchUiBootstrap: any; - feedback: any; } const App = ({ location, user, fetchUiBootstrap }: IAppProps) => { // because we need the userId when the component load. @@ -135,7 +134,6 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => { @@ -147,10 +145,10 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => { ); }; + // Set state to any for now, to avoid typing up entire state object while converting to tsx. const mapStateToProps = (state: any) => ({ user: state.user.toJS(), - feedback: state.feedback, }); export default connect(mapStateToProps)(App); diff --git a/frontend/src/component/addons/AddonForm/AddonEvents/AddonEvents.tsx b/frontend/src/component/addons/AddonForm/AddonEvents/AddonEvents.tsx new file mode 100644 index 0000000000..3beb70432c --- /dev/null +++ b/frontend/src/component/addons/AddonForm/AddonEvents/AddonEvents.tsx @@ -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; +} + +export const AddonEvents = ({ + provider, + checkedEvents, + setEventValue, + error, +}: IAddonProps) => { + if (!provider) return null; + + return ( + +

Events

+ {error} + + {provider.events.map(e => ( + + + } + label={e} + /> + + ))} + +
+ ); +}; diff --git a/frontend/src/component/addons/form-addon-component.jsx b/frontend/src/component/addons/AddonForm/AddonForm.jsx similarity index 87% rename from frontend/src/component/addons/form-addon-component.jsx rename to frontend/src/component/addons/AddonForm/AddonForm.jsx index e4483d6593..d47e76c6d1 100644 --- a/frontend/src/component/addons/form-addon-component.jsx +++ b/frontend/src/component/addons/AddonForm/AddonForm.jsx @@ -1,23 +1,29 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { TextField, FormControlLabel, Switch } from '@material-ui/core'; - -import { FormButtons, styles as commonStyles } from '../common'; -import { trim } from '../common/util'; -import AddonParameters from './form-addon-parameters'; -import AddonEvents from './form-addon-events'; +import { FormButtons, styles as commonStyles } from '../../common'; +import { trim } from '../../common/util'; +import { AddonParameters } from './AddonParameters/AddonParameters'; +import { AddonEvents } from './AddonEvents/AddonEvents'; import cloneDeep from 'lodash.clonedeep'; - -import styles from './form-addon-component.module.scss'; -import PageContent from '../common/PageContent/PageContent'; -import useAddonsApi from '../../hooks/api/actions/useAddonsApi/useAddonsApi'; -import useToast from '../../hooks/useToast'; +import PageContent from '../../common/PageContent/PageContent'; import { useHistory } from 'react-router-dom'; +import useAddonsApi from '../../../hooks/api/actions/useAddonsApi/useAddonsApi'; +import useToast from '../../../hooks/useToast'; +import { makeStyles } from '@material-ui/styles'; -const AddonFormComponent = ({ editMode, provider, addon, fetch }) => { +const useStyles = makeStyles(theme => ({ + nameInput: { + marginRight: '1.5rem', + }, + formSection: { padding: '10px 28px' }, +})); + +export const AddonForm = ({ editMode, provider, addon, fetch }) => { const { createAddon, updateAddon } = useAddonsApi(); const { setToastData, setToastApiError } = useToast(); const history = useHistory(); + const styles = useStyles(); const [config, setConfig] = useState(addon); const [errors, setErrors] = useState({ @@ -116,6 +122,7 @@ const AddonFormComponent = ({ editMode, provider, addon, fetch }) => { history.push('/addons'); setToastData({ type: 'success', + confetti: true, title: 'Addon created successfully', }); } @@ -196,14 +203,17 @@ const AddonFormComponent = ({ editMode, provider, addon, fetch }) => { />
- +
); }; -AddonFormComponent.propTypes = { +AddonForm.propTypes = { provider: PropTypes.object, addon: PropTypes.object.isRequired, fetch: PropTypes.func.isRequired, @@ -211,5 +221,3 @@ AddonFormComponent.propTypes = { cancel: PropTypes.func.isRequired, editMode: PropTypes.bool.isRequired, }; - -export default AddonFormComponent; diff --git a/frontend/src/component/addons/AddonForm/AddonParameters/AddonParameter/AddonParameter.tsx b/frontend/src/component/addons/AddonForm/AddonParameters/AddonParameter/AddonParameter.tsx new file mode 100644 index 0000000000..40441955f8 --- /dev/null +++ b/frontend/src/component/addons/AddonForm/AddonParameters/AddonParameter/AddonParameter.tsx @@ -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; + 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 ( +
+ +
+ ); +}; diff --git a/frontend/src/component/addons/AddonForm/AddonParameters/AddonParameters.tsx b/frontend/src/component/addons/AddonForm/AddonParameters/AddonParameters.tsx new file mode 100644 index 0000000000..10536f5bd2 --- /dev/null +++ b/frontend/src/component/addons/AddonForm/AddonParameters/AddonParameters.tsx @@ -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; + editMode: boolean; + setParameterValue: (param: string) => void; + config: IAddonConfig; +} + +export const AddonParameters = ({ + provider, + config, + errors, + setParameterValue, + editMode, +}: IAddonParametersProps) => { + if (!provider) return null; + + return ( + +

Parameters

+ {editMode ? ( +

+ Sensitive parameters will be masked with value "***** + ". If you don't change the value they will not be updated + when saving. +

+ ) : null} + {provider.parameters.map(parameter => ( + + ))} +
+ ); +}; diff --git a/frontend/src/component/addons/AddonList/AddonList.jsx b/frontend/src/component/addons/AddonList/AddonList.tsx similarity index 69% rename from frontend/src/component/addons/AddonList/AddonList.jsx rename to frontend/src/component/addons/AddonList/AddonList.tsx index a849d2672b..8d96e5c3d0 100644 --- a/frontend/src/component/addons/AddonList/AddonList.jsx +++ b/frontend/src/component/addons/AddonList/AddonList.tsx @@ -1,12 +1,9 @@ -import { useContext, useEffect } from 'react'; -import ConfiguredAddons from './ConfiguredAddons'; -import AvailableAddons from './AvailableAddons'; +import { ReactElement } from 'react'; +import { ConfiguredAddons } from './ConfiguredAddons/ConfiguredAddons'; +import { AvailableAddons } from './AvailableAddons/AvailableAddons'; import { Avatar } from '@material-ui/core'; import { DeviceHub } from '@material-ui/icons'; - import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; -import AccessContext from '../../../contexts/AccessContext'; - import slackIcon from '../../../assets/icons/slack.svg'; import jiraIcon from '../../../assets/icons/jira.svg'; import webhooksIcon from '../../../assets/icons/webhooks.svg'; @@ -14,7 +11,6 @@ import teamsIcon from '../../../assets/icons/teams.svg'; import dataDogIcon from '../../../assets/icons/datadog.svg'; import { formatAssetPath } from '../../../utils/format-path'; import useAddons from '../../../hooks/api/getters/useAddons/useAddons'; -import { useHistory } from 'react-router-dom'; const style = { width: '40px', @@ -23,7 +19,7 @@ const style = { float: 'left', }; -const getIcon = name => { +const getAddonIcon = (name: string): ReactElement => { switch (name) { case 'slack': return ( @@ -74,40 +70,21 @@ const getIcon = name => { } }; -const AddonList = () => { - const { hasAccess } = useContext(AccessContext); - const { addons, providers, refetchAddons } = useAddons(); - const history = useHistory(); - - useEffect(() => { - if (addons.length === 0) { - refetchAddons(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [addons.length]); +export const AddonList = () => { + const { providers, addons } = useAddons(); return ( <> 0} - show={ - - } + show={} />
); }; - -export default AddonList; diff --git a/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.jsx b/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.jsx deleted file mode 100644 index 07adcafc07..0000000000 --- a/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.jsx +++ /dev/null @@ -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 => ( - - {getIcon(provider.name)} - - - - history.push(`/addons/create/${provider.name}`) - } - title="Configure" - > - Configure - - } - /> - - - ); - return ( - - {providers.map(provider => renderProvider(provider))} - - ); -}; - -AvailableAddons.propTypes = { - providers: PropTypes.array.isRequired, - getIcon: PropTypes.func.isRequired, - hasAccess: PropTypes.func.isRequired, - history: PropTypes.object.isRequired, -}; - -export default AvailableAddons; diff --git a/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.tsx b/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.tsx new file mode 100644 index 0000000000..9632b0e9e5 --- /dev/null +++ b/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.tsx @@ -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) => ( + + {getAddonIcon(provider.name)} + + + + history.push(`/addons/create/${provider.name}`) + } + tooltip={`Configure ${provider.name} Addon`} + > + Configure + + + + ); + return ( + + + {providers.map((provider: IProvider) => + renderProvider(provider) + )} + + + ); +}; diff --git a/frontend/src/component/addons/AddonList/AvailableAddons/index.jsx b/frontend/src/component/addons/AddonList/AvailableAddons/index.jsx deleted file mode 100644 index 00f3612923..0000000000 --- a/frontend/src/component/addons/AddonList/AvailableAddons/index.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import AvailableAddons from './AvailableAddons'; - -export default AvailableAddons; diff --git a/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.jsx b/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx similarity index 51% rename from frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.jsx rename to frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx index 63db383913..233d57354b 100644 --- a/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.jsx +++ b/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx @@ -1,6 +1,4 @@ -import React from 'react'; import { - IconButton, List, ListItem, ListItemAvatar, @@ -8,7 +6,6 @@ import { ListItemText, } from '@material-ui/core'; import { Visibility, VisibilityOff, Delete } from '@material-ui/icons'; - import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender'; import { DELETE_ADDON, @@ -16,17 +13,43 @@ import { } from '../../../providers/AccessProvider/permissions'; import { Link } from 'react-router-dom'; import PageContent from '../../../common/PageContent/PageContent'; -import PropTypes from 'prop-types'; import useAddons from '../../../../hooks/api/getters/useAddons/useAddons'; import useToast from '../../../../hooks/useToast'; import useAddonsApi from '../../../../hooks/api/actions/useAddonsApi/useAddonsApi'; +import { ReactElement, useContext, useState } from 'react'; +import AccessContext from '../../../../contexts/AccessContext'; +import { IAddon } from '../../../../interfaces/addons'; +import PermissionIconButton from '../../../common/PermissionIconButton/PermissionIconButton'; +import Dialogue from '../../../common/Dialogue'; -const ConfiguredAddons = ({ addons, hasAccess, getIcon }) => { - const { refetchAddons } = useAddons(); +interface IConfigureAddonsProps { + getAddonIcon: (name: string) => ReactElement; +} + +export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => { + const { refetchAddons, addons } = useAddons(); const { updateAddon, removeAddon } = useAddonsApi(); const { setToastData, setToastApiError } = useToast(); + const { hasAccess } = useContext(AccessContext); + const [showDelete, setShowDelete] = useState(false); + const [deletedAddon, setDeletedAddon] = useState({ + id: 0, + provider: '', + description: '', + enabled: false, + events: [], + parameters: {}, + }); - const toggleAddon = async addon => { + const sortAddons = (addons: IAddon[]) => { + if (!addons) return []; + + return addons.sort((addonA: IAddon, addonB: IAddon) => { + return addonA.id - addonB.id; + }); + }; + + const toggleAddon = async (addon: IAddon) => { try { await updateAddon({ ...addon, enabled: !addon.enabled }); refetchAddons(); @@ -35,12 +58,12 @@ const ConfiguredAddons = ({ addons, hasAccess, getIcon }) => { title: 'Success', text: 'Addon state switched successfully', }); - } catch (e) { + } catch (e: any) { setToastApiError(e.toString()); } }; - const onRemoveAddon = addon => async () => { + const onRemoveAddon = async (addon: IAddon) => { try { await removeAddon(addon.id); refetchAddons(); @@ -58,9 +81,9 @@ const ConfiguredAddons = ({ addons, hasAccess, getIcon }) => { } }; - const renderAddon = addon => ( + const renderAddon = (addon: IAddon) => ( - {getIcon(addon.provider)} + {getAddonIcon(addon.provider)} @@ -85,50 +108,48 @@ const ConfiguredAddons = ({ addons, hasAccess, getIcon }) => { secondary={addon.description} /> - toggleAddon(addon)} - > - } - elseShow={} - /> - - } - /> - - - - } - /> + toggleAddon(addon)} + > + } + elseShow={} + /> + + { + setDeletedAddon(addon); + setShowDelete(true); + }} + > + + ); return ( - {addons.map(addon => renderAddon(addon))} + + {sortAddons(addons).map((addon: IAddon) => renderAddon(addon))} + + { + onRemoveAddon(deletedAddon); + setShowDelete(false); + }} + onClose={() => { + setShowDelete(false); + }} + title="Confirm deletion" + > +
Are you sure you want to delete this Addon?
+
); }; -ConfiguredAddons.propTypes = { - addons: PropTypes.array.isRequired, - hasAccess: PropTypes.func.isRequired, - toggleAddon: PropTypes.func.isRequired, - getIcon: PropTypes.func.isRequired, -}; - -export default ConfiguredAddons; diff --git a/frontend/src/component/addons/AddonList/ConfiguredAddons/index.jsx b/frontend/src/component/addons/AddonList/ConfiguredAddons/index.jsx deleted file mode 100644 index 8a8cf00375..0000000000 --- a/frontend/src/component/addons/AddonList/ConfiguredAddons/index.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import ConfiguredAddons from './ConfiguredAddons'; - -export default ConfiguredAddons; diff --git a/frontend/src/component/addons/AddonList/index.js b/frontend/src/component/addons/AddonList/index.js deleted file mode 100644 index be5a97ab70..0000000000 --- a/frontend/src/component/addons/AddonList/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import AddonListComponent from './AddonList'; - -export default AddonListComponent; diff --git a/frontend/src/component/addons/CreateAddon/CreateAddon.tsx b/frontend/src/component/addons/CreateAddon/CreateAddon.tsx new file mode 100644 index 0000000000..9d18048388 --- /dev/null +++ b/frontend/src/component/addons/CreateAddon/CreateAddon.tsx @@ -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(); + + 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 + + ); +}; diff --git a/frontend/src/component/addons/EditAddon/EditAddon.tsx b/frontend/src/component/addons/EditAddon/EditAddon.tsx new file mode 100644 index 0000000000..9639757bf0 --- /dev/null +++ b/frontend/src/component/addons/EditAddon/EditAddon.tsx @@ -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(); + + 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 + + ); +}; diff --git a/frontend/src/component/addons/form-addon-component.module.scss b/frontend/src/component/addons/form-addon-component.module.scss deleted file mode 100644 index 6145c6cb6e..0000000000 --- a/frontend/src/component/addons/form-addon-component.module.scss +++ /dev/null @@ -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); -} diff --git a/frontend/src/component/addons/form-addon-container.js b/frontend/src/component/addons/form-addon-container.js deleted file mode 100644 index b2a7c721dc..0000000000 --- a/frontend/src/component/addons/form-addon-container.js +++ /dev/null @@ -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; diff --git a/frontend/src/component/addons/form-addon-events.jsx b/frontend/src/component/addons/form-addon-events.jsx deleted file mode 100644 index 1b6f7518d3..0000000000 --- a/frontend/src/component/addons/form-addon-events.jsx +++ /dev/null @@ -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 ( - -

Events

- {error} - - {provider.events.map(e => ( - - } - label={e} - /> - - ))} - -
- ); -}; - -AddonEvents.propTypes = { - provider: PropTypes.object, - checkedEvents: PropTypes.array.isRequired, - setEventValue: PropTypes.func.isRequired, - error: PropTypes.string, -}; - -export default AddonEvents; diff --git a/frontend/src/component/addons/form-addon-parameters.jsx b/frontend/src/component/addons/form-addon-parameters.jsx deleted file mode 100644 index edaef3a141..0000000000 --- a/frontend/src/component/addons/form-addon-parameters.jsx +++ /dev/null @@ -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 ( -
- -
- ); -}; - -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 ( - -

Parameters

- {editMode ? ( -

- Sensitive parameters will be masked with value "***** - ". If you don't change the value they will not be updated when saving. -

- ) : null} - {provider.parameters.map(p => ( - - ))} -
- ); -}; - -AddonParameters.propTypes = { - provider: PropTypes.object, - config: PropTypes.object.isRequired, - errors: PropTypes.object.isRequired, - setParameterValue: PropTypes.func.isRequired, - editMode: PropTypes.bool, -}; - -export default AddonParameters; diff --git a/frontend/src/component/addons/index.jsx b/frontend/src/component/addons/index.jsx deleted file mode 100644 index c04dbe4966..0000000000 --- a/frontend/src/component/addons/index.jsx +++ /dev/null @@ -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; diff --git a/frontend/src/component/admin/api-token/CreateApiToken/CreateApiToken.tsx b/frontend/src/component/admin/api-token/CreateApiToken/CreateApiToken.tsx index 48dbea2295..b100bf1668 100644 --- a/frontend/src/component/admin/api-token/CreateApiToken/CreateApiToken.tsx +++ b/frontend/src/component/admin/api-token/CreateApiToken/CreateApiToken.tsx @@ -12,7 +12,6 @@ import { useState } from 'react'; import { scrollToTop } from '../../../common/util'; const CreateApiToken = () => { - /* @ts-ignore */ const { setToastApiError } = useToast(); const { uiConfig } = useUiConfig(); const history = useHistory(); diff --git a/frontend/src/component/admin/project-roles/CreateProjectRole/CreateProjectRole.tsx b/frontend/src/component/admin/project-roles/CreateProjectRole/CreateProjectRole.tsx index aaab8c7344..78048e0ab0 100644 --- a/frontend/src/component/admin/project-roles/CreateProjectRole/CreateProjectRole.tsx +++ b/frontend/src/component/admin/project-roles/CreateProjectRole/CreateProjectRole.tsx @@ -9,7 +9,6 @@ import PermissionButton from '../../../common/PermissionButton/PermissionButton' import { ADMIN } from '../../../providers/AccessProvider/permissions'; const CreateProjectRole = () => { - /* @ts-ignore */ const { setToastData, setToastApiError } = useToast(); const { uiConfig } = useUiConfig(); const history = useHistory(); diff --git a/frontend/src/component/admin/users/CreateUser/CreateUser.tsx b/frontend/src/component/admin/users/CreateUser/CreateUser.tsx index b49fc833b9..2cc34cda67 100644 --- a/frontend/src/component/admin/users/CreateUser/CreateUser.tsx +++ b/frontend/src/component/admin/users/CreateUser/CreateUser.tsx @@ -12,7 +12,6 @@ import PermissionButton from '../../../common/PermissionButton/PermissionButton' import { ADMIN } from '../../../providers/AccessProvider/permissions'; const CreateUser = () => { - /* @ts-ignore */ const { setToastApiError } = useToast(); const { uiConfig } = useUiConfig(); const history = useHistory(); diff --git a/frontend/src/component/admin/users/UserForm/UserForm.styles.ts b/frontend/src/component/admin/users/UserForm/UserForm.styles.ts index b36d6d2706..a179d251c6 100644 --- a/frontend/src/component/admin/users/UserForm/UserForm.styles.ts +++ b/frontend/src/component/admin/users/UserForm/UserForm.styles.ts @@ -38,7 +38,6 @@ export const useStyles = makeStyles(theme => ({ position: 'relative', }, errorMessage: { - //@ts-ignore fontSize: theme.fontSizes.smallBody, color: theme.palette.error.main, position: 'absolute', diff --git a/frontend/src/component/common/Feedback/Feedback.styles.ts b/frontend/src/component/common/Feedback/Feedback.styles.ts index 19a65e3335..c77541b2c1 100644 --- a/frontend/src/component/common/Feedback/Feedback.styles.ts +++ b/frontend/src/component/common/Feedback/Feedback.styles.ts @@ -4,7 +4,7 @@ export const useStyles = makeStyles(theme => ({ feedback: { borderRadius: '12.5px', backgroundColor: '#fff', - zIndex: '9999', + zIndex: 9999, boxShadow: '2px 2px 4px 4px rgba(143,143,143, 0.25)', padding: '1.5rem', maxWidth: '400px', diff --git a/frontend/src/component/common/Feedback/Feedback.tsx b/frontend/src/component/common/Feedback/Feedback.tsx index 6d2b9d7c48..c926b3c849 100644 --- a/frontend/src/component/common/Feedback/Feedback.tsx +++ b/frontend/src/component/common/Feedback/Feedback.tsx @@ -9,24 +9,21 @@ import { useStyles } from './Feedback.styles'; import AnimateOnMount from '../AnimateOnMount/AnimateOnMount'; import ConditionallyRender from '../ConditionallyRender'; import { formatApiPath } from '../../../utils/format-path'; -import { Action, Dispatch } from 'redux'; import UIContext from '../../../contexts/UIContext'; import useUser from '../../../hooks/api/getters/useUser/useUser'; +import { PNPS_FEEDBACK_ID, showPnpsFeedback } from '../util'; interface IFeedbackProps { - show?: boolean; - hideFeedback: () => Dispatch; - fetchUser: () => void; - feedbackId: string; openUrl: string; } -const Feedback = ({ feedbackId, openUrl }: IFeedbackProps) => { +const Feedback = ({ openUrl }: IFeedbackProps) => { const { showFeedback, setShowFeedback } = useContext(UIContext); const { refetch, feedback } = useUser(); const [answeredNotNow, setAnsweredNotNow] = useState(false); const styles = useStyles(); const commonStyles = useCommonStyles(); + const feedbackId = PNPS_FEEDBACK_ID; const onConfirm = async () => { const url = formatApiPath('api/admin/feedback'); @@ -41,7 +38,8 @@ const Feedback = ({ feedbackId, openUrl }: IFeedbackProps) => { body: JSON.stringify({ feedbackId }), }); await refetch(); - } catch { + } catch (err) { + console.warn(err); setShowFeedback(false); } @@ -68,7 +66,8 @@ const Feedback = ({ feedbackId, openUrl }: IFeedbackProps) => { body: JSON.stringify({ feedbackId, neverShow: true }), }); await refetch(); - } catch { + } catch (err) { + console.warn(err); setShowFeedback(false); } @@ -77,9 +76,7 @@ const Feedback = ({ feedbackId, openUrl }: IFeedbackProps) => { }, 100); }; - const pnps = feedback.find(feedback => feedback.feedbackId === feedbackId); - - if (pnps?.given || pnps?.neverShow) { + if (!showPnpsFeedback(feedback)) { return null; } diff --git a/frontend/src/component/common/FormTemplate/FormTemplate.tsx b/frontend/src/component/common/FormTemplate/FormTemplate.tsx index dd731ae928..14adaef1d4 100644 --- a/frontend/src/component/common/FormTemplate/FormTemplate.tsx +++ b/frontend/src/component/common/FormTemplate/FormTemplate.tsx @@ -24,7 +24,6 @@ const FormTemplate: React.FC = ({ loading, formatApiCode, }) => { - // @ts-ignore-next-line const { setToastData } = useToast(); const styles = useStyles(); const smallScreen = useMediaQuery(`(max-width:${900}px)`); diff --git a/frontend/src/component/common/PaginateUI/PaginateUI.tsx b/frontend/src/component/common/PaginateUI/PaginateUI.tsx index c776481f5d..c2636c589e 100644 --- a/frontend/src/component/common/PaginateUI/PaginateUI.tsx +++ b/frontend/src/component/common/PaginateUI/PaginateUI.tsx @@ -15,6 +15,7 @@ interface IPaginateUIProps { prevPage: () => void; setPageIndex: (idx: number) => void; nextPage: () => void; + style?: React.CSSProperties; } const PaginateUI = ({ diff --git a/frontend/src/component/common/PermissionIconButton/PermissionIconButton.tsx b/frontend/src/component/common/PermissionIconButton/PermissionIconButton.tsx index 903d29713b..ff6103efd2 100644 --- a/frontend/src/component/common/PermissionIconButton/PermissionIconButton.tsx +++ b/frontend/src/component/common/PermissionIconButton/PermissionIconButton.tsx @@ -3,13 +3,19 @@ import { useContext } from 'react'; import AccessContext from '../../../contexts/AccessContext'; interface IPermissionIconButtonProps - extends React.HTMLProps { + extends React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLButtonElement + > { permission: string; Icon?: React.ElementType; tooltip: string; onClick?: (e: any) => void; projectId?: string; environmentId?: string; + edge?: string; + className?: string; + title?: string; } const PermissionIconButton: React.FC = ({ diff --git a/frontend/src/component/common/Proclamation/Proclamation.tsx b/frontend/src/component/common/Proclamation/Proclamation.tsx index b5f3cf2e0d..f34e469eb7 100644 --- a/frontend/src/component/common/Proclamation/Proclamation.tsx +++ b/frontend/src/component/common/Proclamation/Proclamation.tsx @@ -3,16 +3,10 @@ import { Alert } from '@material-ui/lab'; import ConditionallyRender from '../ConditionallyRender'; import { Typography } from '@material-ui/core'; import { useStyles } from './Proclamation.styles'; +import { IProclamationToast } from '../../../interfaces/uiConfig'; interface IProclamationProps { - toast?: IToast; -} - -interface IToast { - message: string; - id: string; - severity: 'success' | 'info' | 'warning' | 'error'; - link: string; + toast?: IProclamationToast; } const renderProclamation = (id: string) => { diff --git a/frontend/src/component/common/Splash/Splash.styles.ts b/frontend/src/component/common/Splash/Splash.styles.ts index 89d64edbe1..2a64b56ccd 100644 --- a/frontend/src/component/common/Splash/Splash.styles.ts +++ b/frontend/src/component/common/Splash/Splash.styles.ts @@ -29,7 +29,7 @@ export const useStyles = makeStyles(theme => ({ right: '0px', bottom: '0px', padding: '2rem 0', - zIndex: '500', + zIndex: 500, position: 'fixed', width: '100%', height: '100%', diff --git a/frontend/src/component/common/ToastRenderer/Toast/Toast.tsx b/frontend/src/component/common/ToastRenderer/Toast/Toast.tsx index 1c11cd10db..0a1d83160e 100644 --- a/frontend/src/component/common/ToastRenderer/Toast/Toast.tsx +++ b/frontend/src/component/common/ToastRenderer/Toast/Toast.tsx @@ -8,7 +8,7 @@ import ConditionallyRender from '../../ConditionallyRender'; import Close from '@material-ui/icons/Close'; const Toast = ({ title, text, type, confetti }: IToastData) => { - // @ts-ignore + // @ts-expect-error const { setToast } = useContext(UIContext); const styles = useStyles(); diff --git a/frontend/src/component/common/ToastRenderer/ToastRenderer.tsx b/frontend/src/component/common/ToastRenderer/ToastRenderer.tsx index 0decb11ff3..924c9dd4a1 100644 --- a/frontend/src/component/common/ToastRenderer/ToastRenderer.tsx +++ b/frontend/src/component/common/ToastRenderer/ToastRenderer.tsx @@ -7,7 +7,7 @@ import AnimateOnMount from '../AnimateOnMount/AnimateOnMount'; import Toast from './Toast/Toast'; const ToastRenderer = () => { - // @ts-ignore-next-line + // @ts-expect-error const { toastData, setToast } = useContext(UIContext); const commonStyles = useCommonStyles(); const styles = useStyles(); diff --git a/frontend/src/component/common/util.js b/frontend/src/component/common/util.js index 4334e09292..cf86b94912 100644 --- a/frontend/src/component/common/util.js +++ b/frontend/src/component/common/util.js @@ -118,12 +118,11 @@ export const modalStyles = { export const updateIndexInArray = (array, index, newValue) => array.map((v, i) => (i === index ? newValue : v)); -export const showPnpsFeedback = user => { - if (!user) return; - if (!user.feedback) return; - if (user.feedback.length > 0) { - const feedback = user.feedback.find( - feedback => feedback.feedbackId === 'pnps' +export const showPnpsFeedback = (feedbackList) => { + if (!feedbackList) return; + if (feedbackList.length > 0) { + const feedback = feedbackList.find( + feedback => feedback.feedbackId === PNPS_FEEDBACK_ID ); if (!feedback) return false; @@ -143,3 +142,5 @@ export const showPnpsFeedback = user => { } return true; }; + +export const PNPS_FEEDBACK_ID = 'pnps' diff --git a/frontend/src/component/context/ContextForm/ContextForm.styles.ts b/frontend/src/component/context/ContextForm/ContextForm.styles.ts index 36fdaf4039..a9f53f195e 100644 --- a/frontend/src/component/context/ContextForm/ContextForm.styles.ts +++ b/frontend/src/component/context/ContextForm/ContextForm.styles.ts @@ -54,7 +54,6 @@ export const useStyles = makeStyles(theme => ({ position: 'relative', }, errorMessage: { - //@ts-ignore fontSize: theme.fontSizes.smallBody, color: theme.palette.error.main, position: 'absolute', diff --git a/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.tsx b/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.tsx index 8cafcb6d68..3c519bb0d2 100644 --- a/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.tsx +++ b/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.tsx @@ -16,7 +16,6 @@ import { ADMIN } from '../../providers/AccessProvider/permissions'; import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions'; const CreateEnvironment = () => { - /* @ts-ignore */ const { setToastApiError, setToastData } = useToast(); const { uiConfig } = useUiConfig(); const history = useHistory(); @@ -76,7 +75,7 @@ const CreateEnvironment = () => { ({ position: 'relative', }, errorMessage: { - //@ts-ignore fontSize: theme.fontSizes.smallBody, color: theme.palette.error.main, position: 'absolute', diff --git a/frontend/src/component/environments/EnvironmentForm/EnvironmentTypeSelector/EnvironmentTypeSelector.styles.ts b/frontend/src/component/environments/EnvironmentForm/EnvironmentTypeSelector/EnvironmentTypeSelector.styles.ts index 3e19ac92b1..11aabd3422 100644 --- a/frontend/src/component/environments/EnvironmentForm/EnvironmentTypeSelector/EnvironmentTypeSelector.styles.ts +++ b/frontend/src/component/environments/EnvironmentForm/EnvironmentTypeSelector/EnvironmentTypeSelector.styles.ts @@ -6,7 +6,6 @@ export const useStyles = makeStyles(theme => ({ }, formHeader: { fontWeight: 'bold', - //@ts-ignore fontSize: theme.fontSizes.bodySize, marginTop: '1.5rem', marginBottom: '0.5rem', diff --git a/frontend/src/component/feature/CreateFeature/CreateFeature.tsx b/frontend/src/component/feature/CreateFeature/CreateFeature.tsx index eceaff9c83..2fe8477254 100644 --- a/frontend/src/component/feature/CreateFeature/CreateFeature.tsx +++ b/frontend/src/component/feature/CreateFeature/CreateFeature.tsx @@ -12,7 +12,6 @@ import { useContext } from 'react'; import UIContext from '../../../contexts/UIContext'; const CreateFeature = () => { - /* @ts-ignore */ const { setToastData, setToastApiError } = useToast(); const { setShowFeedback } = useContext(UIContext); const { uiConfig } = useUiConfig(); diff --git a/frontend/src/component/feature/EditFeature/EditFeature.tsx b/frontend/src/component/feature/EditFeature/EditFeature.tsx index b1cbeeb4dd..bb971d0bcf 100644 --- a/frontend/src/component/feature/EditFeature/EditFeature.tsx +++ b/frontend/src/component/feature/EditFeature/EditFeature.tsx @@ -12,7 +12,6 @@ import PermissionButton from '../../common/PermissionButton/PermissionButton'; import { UPDATE_FEATURE } from '../../providers/AccessProvider/permissions'; const EditFeature = () => { - /* @ts-ignore */ const { setToastData, setToastApiError } = useToast(); const { uiConfig } = useUiConfig(); const history = useHistory(); diff --git a/frontend/src/component/feature/FeatureForm/FeatureForm.styles.ts b/frontend/src/component/feature/FeatureForm/FeatureForm.styles.ts index f7020d7398..2dadac70a1 100644 --- a/frontend/src/component/feature/FeatureForm/FeatureForm.styles.ts +++ b/frontend/src/component/feature/FeatureForm/FeatureForm.styles.ts @@ -38,7 +38,6 @@ export const useStyles = makeStyles(theme => ({ marginBottom: '0.5rem', }, typeDescription: { - //@ts-ignore fontSize: theme.fontSizes.smallBody, color: theme.palette.grey[600], top: '-13px', @@ -55,7 +54,6 @@ export const useStyles = makeStyles(theme => ({ position: 'relative', }, errorMessage: { - //@ts-ignore fontSize: theme.fontSizes.smallBody, color: theme.palette.error.main, position: 'absolute', diff --git a/frontend/src/component/feature/FeatureToggleListNew/FeatureToggleListNew.tsx b/frontend/src/component/feature/FeatureToggleListNew/FeatureToggleListNew.tsx index 9b6b3f73cf..eaebceccea 100644 --- a/frontend/src/component/feature/FeatureToggleListNew/FeatureToggleListNew.tsx +++ b/frontend/src/component/feature/FeatureToggleListNew/FeatureToggleListNew.tsx @@ -24,7 +24,7 @@ interface IFeatureToggleListNewProps { projectId: string; } -//@ts-ignore +// @ts-expect-error const sortList = (list, sortOpt) => { if (!list) { return list; @@ -33,7 +33,7 @@ const sortList = (list, sortOpt) => { return list; } if (sortOpt.type === 'string') { - //@ts-ignore + // @ts-expect-error return list.sort((a, b) => { const fieldA = a[sortOpt.field]?.toUpperCase(); const fieldB = b[sortOpt.field]?.toUpperCase(); @@ -49,7 +49,7 @@ const sortList = (list, sortOpt) => { }); } if (sortOpt.type === 'date') { - //@ts-ignore + // @ts-expect-error return list.sort((a, b) => { const fieldA = new Date(a[sortOpt.field]); const fieldB = new Date(b[sortOpt.field]); diff --git a/frontend/src/component/feature/FeatureView/FeatureStatus/FeatureStatus.tsx b/frontend/src/component/feature/FeatureView/FeatureStatus/FeatureStatus.tsx index 1a954982fb..b7b25230c9 100644 --- a/frontend/src/component/feature/FeatureView/FeatureStatus/FeatureStatus.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureStatus/FeatureStatus.tsx @@ -75,7 +75,6 @@ const FeatureStatus = ({ { const styles = useStyles(); const commonStyles = useCommonStyles(); - const contextNames = contextDefinitions.map(c => ({ + + const { context } = useUnleashContext(); + const contextNames = context.map(c => ({ key: c.name, label: c.name, })); @@ -34,9 +34,7 @@ const OverrideConfig = ({ }; return overrides.map((o, i) => { - const definition = contextDefinitions.find( - c => c.name === o.contextName - ); + const definition = context.find(c => c.name === o.contextName); const legalValues = definition ? definition.legalValues : []; return ( @@ -115,9 +113,3 @@ OverrideConfig.propTypes = { updateOverrideValues: PropTypes.func.isRequired, removeOverride: PropTypes.func.isRequired, }; - -const mapStateToProps = state => ({ - contextDefinitions: state.context.toJS(), -}); - -export default connect(mapStateToProps, {})(OverrideConfig); diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureVariantsList/FeatureVariantsList.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureVariantsList/FeatureVariantsList.tsx index d5c18af65e..5eab68a83f 100644 --- a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureVariantsList/FeatureVariantsList.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureVariantsList/FeatureVariantsList.tsx @@ -147,7 +147,6 @@ const FeatureOverviewVariants = () => { try { const res = await patchFeatureVariants(projectId, featureId, patch); - // @ts-ignore const { variants } = await res.json(); mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false); setToastData({ @@ -204,7 +203,6 @@ const FeatureOverviewVariants = () => { if (patch.length === 0) return; try { const res = await patchFeatureVariants(projectId, featureId, patch); - // @ts-ignore const { variants } = await res.json(); mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false); setToastData({ diff --git a/frontend/src/component/layout/LayoutPicker/LayoutPicker.jsx b/frontend/src/component/layout/LayoutPicker/LayoutPicker.jsx index ccea60b3f4..9b86fd9cd0 100644 --- a/frontend/src/component/layout/LayoutPicker/LayoutPicker.jsx +++ b/frontend/src/component/layout/LayoutPicker/LayoutPicker.jsx @@ -1,6 +1,6 @@ import ConditionallyRender from '../../common/ConditionallyRender'; import { matchPath } from 'react-router'; -import MainLayout from '../MainLayout'; +import { MainLayout } from '../MainLayout/MainLayout'; const LayoutPicker = ({ children, location }) => { const standalonePages = () => { diff --git a/frontend/src/component/layout/MainLayout/MainLayout.jsx b/frontend/src/component/layout/MainLayout/MainLayout.tsx similarity index 81% rename from frontend/src/component/layout/MainLayout/MainLayout.jsx rename to frontend/src/component/layout/MainLayout/MainLayout.tsx index 6b3c836c5a..525c9e225a 100644 --- a/frontend/src/component/layout/MainLayout/MainLayout.jsx +++ b/frontend/src/component/layout/MainLayout/MainLayout.tsx @@ -1,9 +1,7 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { ReactNode } from 'react'; import classnames from 'classnames'; -import { makeStyles } from '@material-ui/styles'; +import { makeStyles } from '@material-ui/core/styles'; import { Grid } from '@material-ui/core'; - import styles from '../../styles.module.scss'; import ErrorContainer from '../../error/error-container'; import Header from '../../menu/Header/Header'; @@ -11,6 +9,7 @@ import Footer from '../../menu/Footer/Footer'; import Proclamation from '../../common/Proclamation/Proclamation'; import BreadcrumbNav from '../../common/BreadcrumbNav/BreadcrumbNav'; import { ReactComponent as Texture } from '../../../assets/img/texture.svg'; +import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig'; const useStyles = makeStyles(theme => ({ container: { @@ -27,18 +26,23 @@ const useStyles = makeStyles(theme => ({ }, })); -const MainLayout = ({ children, location, uiConfig }) => { +interface IMainLayoutProps { + children: ReactNode +} + +export const MainLayout = ({ children }: IMainLayoutProps) => { const muiStyles = useStyles(); + const { uiConfig } = useUiConfig(); return ( <> -
+
@@ -52,7 +56,7 @@ const MainLayout = ({ children, location, uiConfig }) => { position: 'fixed', right: '0', bottom: '-4px', - zIndex: '1', + zIndex: 1, }} > @@ -64,9 +68,3 @@ const MainLayout = ({ children, location, uiConfig }) => { ); }; - -MainLayout.propTypes = { - location: PropTypes.object.isRequired, -}; - -export default MainLayout; diff --git a/frontend/src/component/layout/MainLayout/index.js b/frontend/src/component/layout/MainLayout/index.js deleted file mode 100644 index 04ae6cce09..0000000000 --- a/frontend/src/component/layout/MainLayout/index.js +++ /dev/null @@ -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); diff --git a/frontend/src/component/menu/Header/Header.styles.ts b/frontend/src/component/menu/Header/Header.styles.ts index 091b89226a..410b6787e6 100644 --- a/frontend/src/component/menu/Header/Header.styles.ts +++ b/frontend/src/component/menu/Header/Header.styles.ts @@ -7,7 +7,7 @@ export const useStyles = makeStyles(theme => ({ padding: '0.5rem', boxShadow: 'none', position: 'relative', - zIndex: '300', + zIndex: 300, }, links: { display: 'flex', diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap index 062bc1ea4c..f643fa4014 100644 --- a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap +++ b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap @@ -286,7 +286,7 @@ Array [ "layout": "main", "menu": Object {}, "parent": "/addons", - "path": "/addons/create/:provider", + "path": "/addons/create/:providerId", "title": "Create", "type": "protected", }, @@ -295,7 +295,7 @@ Array [ "layout": "main", "menu": Object {}, "parent": "/addons", - "path": "/addons/edit/:id", + "path": "/addons/edit/:addonId", "title": "Edit", "type": "protected", }, diff --git a/frontend/src/component/menu/routes.js b/frontend/src/component/menu/routes.js index b0724648e5..22d66f43a1 100644 --- a/frontend/src/component/menu/routes.js +++ b/frontend/src/component/menu/routes.js @@ -6,10 +6,8 @@ import Strategies from '../../page/strategies'; import HistoryPage from '../../page/history'; import HistoryTogglePage from '../../page/history/toggle'; import { ArchiveListContainer } from '../archive/ArchiveListContainer'; -import ListTagTypes from '../../page/tag-types'; -import Addons from '../../page/addons'; -import AddonsCreate from '../../page/addons/create'; -import AddonsEdit from '../../page/addons/edit'; +import { TagTypeList } from '../tags/TagTypeList/TagTypeList'; +import { AddonList } from '../addons/AddonList/AddonList'; import Admin from '../admin'; import AdminApi from '../admin/api'; import AdminInvoice from '../admin/invoice/InvoiceAdminPage'; @@ -35,8 +33,8 @@ import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironme import EditEnvironment from '../environments/EditEnvironment/EditEnvironment'; import CreateContext from '../context/CreateContext/CreateContext'; import EditContext from '../context/EditContext/EditContext'; -import EditTagType from '../tagTypes/EditTagType/EditTagType'; -import CreateTagType from '../tagTypes/CreateTagType/CreateTagType'; +import EditTagType from '../tags/EditTagType/EditTagType'; +import CreateTagType from '../tags/CreateTagType/CreateTagType'; import EditProject from '../project/Project/EditProject/EditProject'; import CreateProject from '../project/Project/CreateProject/CreateProject'; import CreateFeature from '../feature/CreateFeature/CreateFeature'; @@ -45,6 +43,8 @@ import { ApplicationEdit } from '../application/ApplicationEdit/ApplicationEdit' import { ApplicationList } from '../application/ApplicationList/ApplicationList'; import ContextList from '../context/ContextList/ContextList'; import RedirectFeatureView from '../feature/RedirectFeatureView/RedirectFeatureView'; +import { CreateAddon } from '../addons/CreateAddon/CreateAddon'; +import { EditAddon } from '../addons/EditAddon/EditAddon'; export const routes = [ // Project @@ -314,7 +314,7 @@ export const routes = [ { path: '/tag-types', title: 'Tag types', - component: ListTagTypes, + component: TagTypeList, type: 'protected', layout: 'main', menu: { mobile: true, advanced: true }, @@ -322,19 +322,19 @@ export const routes = [ // Addons { - path: '/addons/create/:provider', + path: '/addons/create/:providerId', parent: '/addons', title: 'Create', - component: AddonsCreate, + component: CreateAddon, type: 'protected', layout: 'main', menu: {}, }, { - path: '/addons/edit/:id', + path: '/addons/edit/:addonId', parent: '/addons', title: 'Edit', - component: AddonsEdit, + component: EditAddon, type: 'protected', layout: 'main', menu: {}, @@ -342,7 +342,7 @@ export const routes = [ { path: '/addons', title: 'Addons', - component: Addons, + component: AddonList, hidden: false, type: 'protected', layout: 'main', diff --git a/frontend/src/component/project/Project/CreateProject/CreateProject.tsx b/frontend/src/component/project/Project/CreateProject/CreateProject.tsx index c22425b5ad..eed75ece87 100644 --- a/frontend/src/component/project/Project/CreateProject/CreateProject.tsx +++ b/frontend/src/component/project/Project/CreateProject/CreateProject.tsx @@ -10,7 +10,6 @@ import PermissionButton from '../../../common/PermissionButton/PermissionButton' import { CREATE_PROJECT } from '../../../providers/AccessProvider/permissions'; const CreateProject = () => { - /* @ts-ignore */ const { setToastData, setToastApiError } = useToast(); const { refetch } = useUser(); const { uiConfig } = useUiConfig(); diff --git a/frontend/src/component/project/Project/Project.tsx b/frontend/src/component/project/Project/Project.tsx index 70c60bd618..f05acc4c40 100644 --- a/frontend/src/component/project/Project/Project.tsx +++ b/frontend/src/component/project/Project/Project.tsx @@ -12,7 +12,7 @@ import useQueryParams from '../../../hooks/useQueryParams'; import { useEffect } from 'react'; import useTabs from '../../../hooks/useTabs'; import TabPanel from '../../common/TabNav/TabPanel'; -import ProjectAccess from '../access-container'; +import { ProjectAccess } from '../ProjectAccess/ProjectAccess'; import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment'; import ProjectOverview from './ProjectOverview'; import ProjectHealth from './ProjectHealth/ProjectHealth'; diff --git a/frontend/src/component/project/Project/ProjectForm/ProjectForm.style.ts b/frontend/src/component/project/Project/ProjectForm/ProjectForm.style.ts index e5e5e19a86..6b8a221e3a 100644 --- a/frontend/src/component/project/Project/ProjectForm/ProjectForm.style.ts +++ b/frontend/src/component/project/Project/ProjectForm/ProjectForm.style.ts @@ -38,7 +38,7 @@ export const useStyles = makeStyles(theme => ({ position: 'relative', }, errorMessage: { - //@ts-ignore + // @ts-expect-error fontSize: theme.fontSizes.smallBody, color: theme.palette.error.main, position: 'absolute', diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccess.styles.ts b/frontend/src/component/project/ProjectAccess/ProjectAccess.styles.ts index 388a9a9da1..d34ce9b1d1 100644 --- a/frontend/src/component/project/ProjectAccess/ProjectAccess.styles.ts +++ b/frontend/src/component/project/ProjectAccess/ProjectAccess.styles.ts @@ -11,18 +11,11 @@ export const useStyles = makeStyles(theme => ({ backgroundColor: '#efefef', marginTop: '2rem', }, - actionList: { - display: 'flex', - alignItems: 'center', - }, inputLabel: { backgroundColor: '#fff' }, roleName: { fontWeight: 'bold', padding: '5px 0px', }, - iconButton: { - marginLeft: '0.5rem', - }, menuItem: { width: '340px', whiteSpace: 'normal', diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccess.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccess.tsx index fee3f58a6d..48ac53420d 100644 --- a/frontend/src/component/project/ProjectAccess/ProjectAccess.tsx +++ b/frontend/src/component/project/ProjectAccess/ProjectAccess.tsx @@ -1,71 +1,40 @@ /* eslint-disable react/jsx-no-target-blank */ -import { useEffect, useState } from 'react'; -import { - Avatar, - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - List, - ListItem, - ListItemAvatar, - ListItemSecondaryAction, - ListItemText, - MenuItem, -} from '@material-ui/core'; -import { Delete } from '@material-ui/icons'; +import React, { useState } from 'react'; import { Alert } from '@material-ui/lab'; -import AddUserComponent from '../access-add-user'; +import { ProjectAccessAddUser } from './ProjectAccessAddUser/ProjectAccessAddUser'; -import projectApi from '../../../store/project/api'; import PageContent from '../../common/PageContent'; import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig'; import { useStyles } from './ProjectAccess.styles'; -import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton'; import { useParams } from 'react-router-dom'; -import { IFeatureViewParams } from '../../../interfaces/params'; -import ProjectRoleSelect from './ProjectRoleSelect/ProjectRoleSelect'; +import { IProjectViewParams } from '../../../interfaces/params'; import usePagination from '../../../hooks/usePagination'; import PaginateUI from '../../common/PaginateUI/PaginateUI'; import useToast from '../../../hooks/useToast'; import ConfirmDialogue from '../../common/Dialogue'; +import useProjectAccess, { + IProjectAccessUser, +} from '../../../hooks/api/getters/useProjectAccess/useProjectAccess'; +import useProjectApi from '../../../hooks/api/actions/useProjectApi/useProjectApi'; +import HeaderTitle from '../../common/HeaderTitle'; +import { ProjectAccessList } from './ProjectAccessList/ProjectAccessList'; -const ProjectAccess = () => { - const { id } = useParams(); +export const ProjectAccess = () => { + const { id: projectId } = useParams(); const styles = useStyles(); - const [roles, setRoles] = useState([]); - const [users, setUsers] = useState([]); - const [error, setError] = useState(); - const { setToastData, setToastApiError } = useToast(); + const { access, refetchProjectAccess } = useProjectAccess(projectId); + const { setToastData } = useToast(); const { isOss } = useUiConfig(); const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } = - usePagination(users, 10); + usePagination(access.users, 10); + const { removeUserFromRole, addUserToRole } = useProjectApi(); const [showDelDialogue, setShowDelDialogue] = useState(false); - const [user, setUser] = useState({}); - - useEffect(() => { - fetchAccess(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [id]); - - const fetchAccess = async () => { - try { - const access = await projectApi.fetchAccess(id); - setRoles(access.roles); - setUsers( - access.users.map(u => ({ ...u, name: u.name || '(No name)' })) - ); - } catch (e) { - setToastApiError(e.toString()); - } - }; + const [user, setUser] = useState(); if (isOss()) { return ( - + }> Controlling access to projects requires a paid version of Unleash. Check out{' '} @@ -78,58 +47,49 @@ const ProjectAccess = () => { ); } - const handleRoleChange = (userId, currRoleId) => async evt => { - const roleId = evt.target.value; - try { - await projectApi.removeUserFromRole(id, currRoleId, userId); - await projectApi.addUserToRole(id, roleId, userId).then(() => { + const handleRoleChange = + (userId: number, currRoleId: number) => + async ( + evt: React.ChangeEvent<{ + name?: string; + value: unknown; + }> + ) => { + const roleId = Number(evt.target.value); + try { + await removeUserFromRole(projectId, currRoleId, userId); + await addUserToRole(projectId, roleId, userId); + refetchProjectAccess(); + setToastData({ type: 'success', title: 'User role changed successfully', }); - }); - const newUsers = users.map(u => { - if (u.id === userId) { - return { ...u, roleId }; - } else return u; - }); - setUsers(newUsers); - } catch (err) { - setToastData({ - type: 'error', - title: err.message || 'Server problems when adding users.', - }); - } + } catch (err: any) { + setToastData({ + type: 'error', + title: err.message || 'Server problems when adding users.', + }); + } + }; + + const handleRemoveAccess = (user: IProjectAccessUser) => { + setUser(user); + setShowDelDialogue(true); }; - const addUser = async (userId, roleId) => { - try { - await projectApi.addUserToRole(id, roleId, userId); - await fetchAccess().then(() => { - setToastData({ - type: 'success', - title: 'Successfully added user to the project', - }); - }); - } catch (err) { - setToastData({ - type: 'error', - title: err.message || 'Server problems when adding users.', - }); - } - }; + const removeAccess = (user: IProjectAccessUser | undefined) => async () => { + if (!user) return; + const { id, roleId } = user; - const removeAccess = (userId: number, roleId: number) => async () => { try { - await projectApi.removeUserFromRole(id, roleId, userId).then(() => { - setToastData({ - type: 'success', - title: 'User have been removed from project', - }); + await removeUserFromRole(projectId, roleId, id); + refetchProjectAccess(); + setToastData({ + type: 'success', + title: 'The user has been removed from project', }); - const newUsers = users.filter(u => u.id !== userId); - setUsers(newUsers); - } catch (err) { + } catch (err: any) { setToastData({ type: 'error', title: err.message || 'Server problems when adding users.', @@ -138,91 +98,20 @@ const ProjectAccess = () => { setShowDelDialogue(false); }; - const handleCloseError = () => { - setError(undefined); - }; - return ( - - - - {'Error'} - - - {error} - - - - - - -
- - {page.map(user => { - const labelId = `checkbox-list-secondary-label-${user.id}`; - return ( - - - - - - - - - Choose role - - + } + className={styles.pageContent} + > + - { - setUser(user); - setShowDelDialogue(true); - }} - disabled={users.length === 1} - tooltip={ - users.length === 1 - ? 'A project must have at least one owner' - : 'Remove access' - } - > - - - - - ); - })} +
+ { prevPage={prevPage} style={{ bottom: '-21px' }} /> -
+ + { - setUser({}); + setUser(undefined); setShowDelDialogue(false); }} title="Really remove user from this project" @@ -244,5 +134,3 @@ const ProjectAccess = () => {
); }; - -export default ProjectAccess; diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessAddUser/ProjectAccessAddUser.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessAddUser/ProjectAccessAddUser.tsx new file mode 100644 index 0000000000..26116a29f9 --- /dev/null +++ b/frontend/src/component/project/ProjectAccess/ProjectAccessAddUser/ProjectAccessAddUser.tsx @@ -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(); + const [role, setRole] = useState(); + 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 ( + <> + + The user must have an Unleash root role before added to the + project. + + + + handleBlur()} + value={user || ''} + freeSolo + getOptionSelected={() => true} + filterOptions={o => o} + getOptionLabel={getOptionLabel} + options={options} + loading={loading} + renderInput={params => ( + + + + ), + endAdornment: ( + <> + + } + /> + + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + + + + + + + + + + ); +}; diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessList.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessList.tsx new file mode 100644 index 0000000000..42c15e3b42 --- /dev/null +++ b/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessList.tsx @@ -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 = ({ + 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 ( + + {sortUsers(page).map(user => { + return ( + + ); + })} + {children} + + ); +}; diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessListItem/ProjectAccessListItem.styles.ts b/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessListItem/ProjectAccessListItem.styles.ts new file mode 100644 index 0000000000..72f94047e0 --- /dev/null +++ b/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessListItem/ProjectAccessListItem.styles.ts @@ -0,0 +1,11 @@ +import { makeStyles } from '@material-ui/core/styles'; + +export const useStyles = makeStyles(() => ({ + iconButton: { + marginLeft: '0.5rem', + }, + actionList: { + display: 'flex', + alignItems: 'center', + }, +})); diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessListItem/ProjectAccessListItem.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessListItem/ProjectAccessListItem.tsx new file mode 100644 index 0000000000..be16175c34 --- /dev/null +++ b/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessListItem/ProjectAccessListItem.tsx @@ -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(); + const styles = useStyles(); + + const labelId = `checkbox-list-secondary-label-${user.id}`; + + return ( + + + + + + + + + Choose role + + + { + handleRemoveAccess(user); + }} + disabled={access.users.length === 1} + tooltip={ + access.users.length === 1 + ? 'A project must have at least one owner' + : 'Remove access' + } + > + + + + + ); +}; diff --git a/frontend/src/component/project/ProjectAccess/ProjectRoleSelect/ProjectRoleSelect.tsx b/frontend/src/component/project/ProjectAccess/ProjectRoleSelect/ProjectRoleSelect.tsx index deae3eda52..c8f7dcf257 100644 --- a/frontend/src/component/project/ProjectAccess/ProjectRoleSelect/ProjectRoleSelect.tsx +++ b/frontend/src/component/project/ProjectAccess/ProjectRoleSelect/ProjectRoleSelect.tsx @@ -1,19 +1,24 @@ import { FormControl, InputLabel, Select, MenuItem } from '@material-ui/core'; import React from 'react'; -import IRole from '../../../../interfaces/role'; +import { IProjectRole } from '../../../../interfaces/role'; import { useStyles } from '../ProjectAccess.styles'; interface IProjectRoleSelect { - roles: IRole[]; + roles: IProjectRole[]; labelId: string; id: string; placeholder?: string; - onChange: () => void; + onChange: ( + evt: React.ChangeEvent<{ + name?: string | undefined; + value: unknown; + }> + ) => void; value: any; } -const ProjectRoleSelect: React.FC = ({ +export const ProjectRoleSelect: React.FC = ({ roles, onChange, labelId, @@ -39,9 +44,10 @@ const ProjectRoleSelect: React.FC = ({ value={value || ''} onChange={onChange} renderValue={roleId => { - return roles?.find(role => { + const role = roles?.find(role => { return role.id === roleId; - }).name; + }); + return role?.name || ''; }} > {children} @@ -66,5 +72,3 @@ const ProjectRoleSelect: React.FC = ({ ); }; - -export default ProjectRoleSelect; diff --git a/frontend/src/component/project/access-add-user.tsx b/frontend/src/component/project/access-add-user.tsx deleted file mode 100644 index e5029160c9..0000000000 --- a/frontend/src/component/project/access-add-user.tsx +++ /dev/null @@ -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 ( - <> - - The user must have an Unleash root role before added to the - project. - - - - 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 => ( - - - - ), - endAdornment: ( - - {loading ? ( - - ) : null} - {params.InputProps.endAdornment} - - ), - }} - /> - )} - /> - - - - - - - - - - ); -} - -AddUserComponent.propTypes = { - roles: PropTypes.array.isRequired, - addUserToRole: PropTypes.func.isRequired, -}; - -export default AddUserComponent; diff --git a/frontend/src/component/project/access-container.js b/frontend/src/component/project/access-container.js deleted file mode 100644 index 692a07f425..0000000000 --- a/frontend/src/component/project/access-container.js +++ /dev/null @@ -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; diff --git a/frontend/src/component/tag-types/TagTypeList/index.jsx b/frontend/src/component/tag-types/TagTypeList/index.jsx deleted file mode 100644 index 20507a5dfc..0000000000 --- a/frontend/src/component/tag-types/TagTypeList/index.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import TagTypeList from './TagTypeList'; - -export default TagTypeList; diff --git a/frontend/src/component/tag-types/__tests__/__snapshots__/tag-type-create-component-test.js.snap b/frontend/src/component/tag-types/__tests__/__snapshots__/tag-type-create-component-test.js.snap deleted file mode 100644 index 29b3b14a98..0000000000 --- a/frontend/src/component/tag-types/__tests__/__snapshots__/tag-type-create-component-test.js.snap +++ /dev/null @@ -1,157 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`it supports editMode 1`] = ` -
-
-
-
-

- Update Tag type -

-
-
-
-
-
-
- Tag types allow you to group tags together in the management UI -
-
- - You do not have permissions to save. - -
-
-
-
-`; - -exports[`renders correctly for creating 1`] = ` -
-
-
-
-

- Create Tag type -

-
-
-
-
-
-
- Tag types allow you to group tags together in the management UI -
-
- - You do not have permissions to save. - -
-
-
-
-`; - -exports[`renders correctly for creating without permissions 1`] = ` -
-
-
-
-

- Create Tag type -

-
-
-
-
-
-
- Tag types allow you to group tags together in the management UI -
-
- - You do not have permissions to save. - -
-
-
-
-`; diff --git a/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js b/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js deleted file mode 100644 index f43bb99bde..0000000000 --- a/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js +++ /dev/null @@ -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( - - - Promise.resolve(true)} - tagType={{ name: '', description: '', icon: '' }} - editMode={false} - submit={jest.fn()} - /> - - - ) - .toJSON(); - expect(tree).toMatchSnapshot(); -}); - -test('renders correctly for creating without permissions', () => { - const tree = renderer - .create( - - - Promise.resolve(true)} - tagType={{ name: '', description: '', icon: '' }} - editMode={false} - submit={jest.fn()} - /> - - - ) - .toJSON(); - expect(tree).toMatchSnapshot(); -}); - -test('it supports editMode', () => { - const tree = renderer - .create( - - - Promise.resolve(true)} - tagType={{ name: '', description: '', icon: '' }} - editMode - submit={jest.fn()} - /> - - - ) - .toJSON(); - expect(tree).toMatchSnapshot(); -}); diff --git a/frontend/src/component/tag-types/create-tag-type-container.js b/frontend/src/component/tag-types/create-tag-type-container.js deleted file mode 100644 index a8cd3c253e..0000000000 --- a/frontend/src/component/tag-types/create-tag-type-container.js +++ /dev/null @@ -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; diff --git a/frontend/src/component/tag-types/edit-tag-type-container.js b/frontend/src/component/tag-types/edit-tag-type-container.js deleted file mode 100644 index 4372dc799b..0000000000 --- a/frontend/src/component/tag-types/edit-tag-type-container.js +++ /dev/null @@ -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; diff --git a/frontend/src/component/tag-types/form-tag-type-component.js b/frontend/src/component/tag-types/form-tag-type-component.js deleted file mode 100644 index 0f07f700d7..0000000000 --- a/frontend/src/component/tag-types/form-tag-type-component.js +++ /dev/null @@ -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 ( - -
- - Tag types allow you to group tags together in the - management UI - -
- setTagTypeName(v.target.value.trim())} - variant="outlined" - size="small" - /> - setTagTypeDescription(v.target.value)} - variant="outlined" - size="small" - /> - - -
- } - elseShow={ - You do not have permissions to save. - } - /> - - - - ); -}; - -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; diff --git a/frontend/src/component/tag-types/index.jsx b/frontend/src/component/tag-types/index.jsx deleted file mode 100644 index 5cd1cf92d5..0000000000 --- a/frontend/src/component/tag-types/index.jsx +++ /dev/null @@ -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; diff --git a/frontend/src/component/tagTypes/CreateTagType/CreateTagType.tsx b/frontend/src/component/tags/CreateTagType/CreateTagType.tsx similarity index 97% rename from frontend/src/component/tagTypes/CreateTagType/CreateTagType.tsx rename to frontend/src/component/tags/CreateTagType/CreateTagType.tsx index 75f589eaab..147129e1b9 100644 --- a/frontend/src/component/tagTypes/CreateTagType/CreateTagType.tsx +++ b/frontend/src/component/tags/CreateTagType/CreateTagType.tsx @@ -5,7 +5,7 @@ import useToast from '../../../hooks/useToast'; import FormTemplate from '../../common/FormTemplate/FormTemplate'; import PermissionButton from '../../common/PermissionButton/PermissionButton'; import { UPDATE_TAG_TYPE } from '../../providers/AccessProvider/permissions'; -import useTagForm from '../hooks/useTagForm'; +import useTagTypeForm from '../TagTypeForm/useTagTypeForm'; import TagTypeForm from '../TagTypeForm/TagTypeForm'; const CreateTagType = () => { @@ -21,7 +21,7 @@ const CreateTagType = () => { validateNameUniqueness, errors, clearErrors, - } = useTagForm(); + } = useTagTypeForm(); const { createTag, loading } = useTagTypesApi(); const handleSubmit = async (e: Event) => { diff --git a/frontend/src/component/tagTypes/EditTagType/EditTagType.tsx b/frontend/src/component/tags/EditTagType/EditTagType.tsx similarity index 95% rename from frontend/src/component/tagTypes/EditTagType/EditTagType.tsx rename to frontend/src/component/tags/EditTagType/EditTagType.tsx index e1bba0ff8d..b8d825f00d 100644 --- a/frontend/src/component/tagTypes/EditTagType/EditTagType.tsx +++ b/frontend/src/component/tags/EditTagType/EditTagType.tsx @@ -6,7 +6,7 @@ import useToast from '../../../hooks/useToast'; import FormTemplate from '../../common/FormTemplate/FormTemplate'; import PermissionButton from '../../common/PermissionButton/PermissionButton'; import { UPDATE_TAG_TYPE } from '../../providers/AccessProvider/permissions'; -import useTagForm from '../hooks/useTagForm'; +import useTagTypeForm from '../TagTypeForm/useTagTypeForm'; import TagForm from '../TagTypeForm/TagTypeForm'; const EditTagType = () => { @@ -23,7 +23,7 @@ const EditTagType = () => { getTagPayload, errors, clearErrors, - } = useTagForm(tagType?.name, tagType?.description); + } = useTagTypeForm(tagType?.name, tagType?.description); const { updateTagType, loading } = useTagTypesApi(); const handleSubmit = async (e: Event) => { diff --git a/frontend/src/component/tags/Tag.module.scss b/frontend/src/component/tags/Tag.module.scss deleted file mode 100644 index b2a967e5a3..0000000000 --- a/frontend/src/component/tags/Tag.module.scss +++ /dev/null @@ -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; -} diff --git a/frontend/src/component/tags/TagList/TagList.jsx b/frontend/src/component/tags/TagList/TagList.jsx deleted file mode 100644 index bc26b0dbf6..0000000000 --- a/frontend/src/component/tags/TagList/TagList.jsx +++ /dev/null @@ -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 => ( - - - - - - } - /> - - ); - - const DeleteButton = ({ tagType, tagValue }) => ( - - remove({ type: tagType, value: tagValue }, e)} - > - - - - ); - - DeleteButton.propTypes = { - tagType: PropTypes.string, - tagValue: PropTypes.string, - }; - - const AddButton = ({ hasAccess }) => ( - history.push('/tag-types/create')} - > - - - } - elseShow={ - - - - } - /> - } - /> - ); - return ( - } - /> - } - > - - 0} - show={tags.map(tag => listItem(tag))} - elseShow={ - - - - } - /> - - - ); -}; - -TagList.propTypes = { - tags: PropTypes.array.isRequired, - fetchTags: PropTypes.func.isRequired, - removeTag: PropTypes.func.isRequired, -}; - -export default TagList; diff --git a/frontend/src/component/tags/TagList/TagList.styles.js b/frontend/src/component/tags/TagList/TagList.styles.js deleted file mode 100644 index 05e9821937..0000000000 --- a/frontend/src/component/tags/TagList/TagList.styles.js +++ /dev/null @@ -1,7 +0,0 @@ -import { makeStyles } from '@material-ui/styles'; - -export const useStyles = makeStyles({ - tagListItem: { - padding: 0, - }, -}); diff --git a/frontend/src/component/tags/TagList/index.jsx b/frontend/src/component/tags/TagList/index.jsx deleted file mode 100644 index bcd0059830..0000000000 --- a/frontend/src/component/tags/TagList/index.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import TagsListComponent from './TagList'; - -export default TagsListComponent; diff --git a/frontend/src/component/tagTypes/TagTypeForm/TagTypeForm.styles.ts b/frontend/src/component/tags/TagTypeForm/TagTypeForm.styles.ts similarity index 98% rename from frontend/src/component/tagTypes/TagTypeForm/TagTypeForm.styles.ts rename to frontend/src/component/tags/TagTypeForm/TagTypeForm.styles.ts index f56f4b180b..5eb051e6c7 100644 --- a/frontend/src/component/tagTypes/TagTypeForm/TagTypeForm.styles.ts +++ b/frontend/src/component/tags/TagTypeForm/TagTypeForm.styles.ts @@ -38,7 +38,6 @@ export const useStyles = makeStyles(theme => ({ position: 'relative', }, errorMessage: { - //@ts-ignore fontSize: theme.fontSizes.smallBody, color: theme.palette.error.main, position: 'absolute', diff --git a/frontend/src/component/tagTypes/TagTypeForm/TagTypeForm.tsx b/frontend/src/component/tags/TagTypeForm/TagTypeForm.tsx similarity index 100% rename from frontend/src/component/tagTypes/TagTypeForm/TagTypeForm.tsx rename to frontend/src/component/tags/TagTypeForm/TagTypeForm.tsx diff --git a/frontend/src/component/tagTypes/hooks/useTagForm.ts b/frontend/src/component/tags/TagTypeForm/useTagTypeForm.ts similarity index 94% rename from frontend/src/component/tagTypes/hooks/useTagForm.ts rename to frontend/src/component/tags/TagTypeForm/useTagTypeForm.ts index feb1df7982..fe7452914e 100644 --- a/frontend/src/component/tagTypes/hooks/useTagForm.ts +++ b/frontend/src/component/tags/TagTypeForm/useTagTypeForm.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi'; -const useTagForm = (initialTagName = '', initialTagDesc = '') => { +const useTagTypeForm = (initialTagName = '', initialTagDesc = '') => { const [tagName, setTagName] = useState(initialTagName); const [tagDesc, setTagDesc] = useState(initialTagDesc); const [errors, setErrors] = useState({}); @@ -66,4 +66,4 @@ const useTagForm = (initialTagName = '', initialTagDesc = '') => { }; }; -export default useTagForm; +export default useTagTypeForm; diff --git a/frontend/src/component/tag-types/TagTypeList/TagTypeList.jsx b/frontend/src/component/tags/TagTypeList/TagTypeList.jsx similarity index 98% rename from frontend/src/component/tag-types/TagTypeList/TagTypeList.jsx rename to frontend/src/component/tags/TagTypeList/TagTypeList.jsx index e8e3cdce38..72bb7d8b12 100644 --- a/frontend/src/component/tag-types/TagTypeList/TagTypeList.jsx +++ b/frontend/src/component/tags/TagTypeList/TagTypeList.jsx @@ -2,12 +2,12 @@ import { useContext, useState } from 'react'; import PropTypes from 'prop-types'; import { Link, useHistory } from 'react-router-dom'; import { + Button, + IconButton, List, ListItem, ListItemIcon, ListItemText, - IconButton, - Button, Tooltip, } from '@material-ui/core'; import { Add, Delete, Edit, Label } from '@material-ui/icons'; @@ -20,14 +20,14 @@ import { } from '../../providers/AccessProvider/permissions'; import Dialogue from '../../common/Dialogue/Dialogue'; import useMediaQuery from '@material-ui/core/useMediaQuery'; -import styles from '../TagType.module.scss'; +import styles from './TagTypeList.module.scss'; import AccessContext from '../../../contexts/AccessContext'; import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi'; import useTagTypes from '../../../hooks/api/getters/useTagTypes/useTagTypes'; import useToast from '../../../hooks/useToast'; import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton'; -const TagTypeList = () => { +export const TagTypeList = () => { const { hasAccess } = useContext(AccessContext); const [deletion, setDeletion] = useState({ open: false }); const history = useHistory(); @@ -160,5 +160,3 @@ TagTypeList.propTypes = { fetchTagTypes: PropTypes.func.isRequired, removeTagType: PropTypes.func.isRequired, }; - -export default TagTypeList; diff --git a/frontend/src/component/tag-types/TagType.module.scss b/frontend/src/component/tags/TagTypeList/TagTypeList.module.scss similarity index 100% rename from frontend/src/component/tag-types/TagType.module.scss rename to frontend/src/component/tags/TagTypeList/TagTypeList.module.scss diff --git a/frontend/src/component/tag-types/__tests__/tag-type-list-component-test.js b/frontend/src/component/tags/TagTypeList/__tests__/TagTypeList.test.js similarity index 82% rename from frontend/src/component/tag-types/__tests__/tag-type-list-component-test.js rename to frontend/src/component/tags/TagTypeList/__tests__/TagTypeList.test.js index 31d648fee7..b91aed95e7 100644 --- a/frontend/src/component/tag-types/__tests__/tag-type-list-component-test.js +++ b/frontend/src/component/tags/TagTypeList/__tests__/TagTypeList.test.js @@ -1,20 +1,18 @@ import React from 'react'; - -import TagTypesList from '../TagTypeList'; +import { TagTypeList } from '../TagTypeList'; import renderer from 'react-test-renderer'; import { MemoryRouter } from 'react-router-dom'; import { ThemeProvider } from '@material-ui/styles'; -import theme from '../../../themes/main-theme'; -import { createFakeStore } from '../../../accessStoreFake'; -import AccessProvider from '../../providers/AccessProvider/AccessProvider'; - +import theme from '../../../../themes/main-theme'; +import { createFakeStore } from '../../../../accessStoreFake'; +import AccessProvider from '../../../providers/AccessProvider/AccessProvider'; import { ADMIN, CREATE_TAG_TYPE, UPDATE_TAG_TYPE, DELETE_TAG_TYPE, -} from '../../providers/AccessProvider/permissions'; -import UIProvider from '../../providers/UIProvider/UIProvider'; +} from '../../../providers/AccessProvider/permissions'; +import UIProvider from '../../../providers/UIProvider/UIProvider'; test('renders an empty list correctly', () => { const tree = renderer.create( @@ -24,7 +22,7 @@ test('renders an empty list correctly', () => { - { { permission: DELETE_TAG_TYPE }, ])} > - ({ - tag: { type: '', value: '' }, -}); - -const mapDispatchToProps = dispatch => ({ - submit: tag => create(tag)(dispatch), -}); - -const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(TagComponent); - -export default FormAddContainer; diff --git a/frontend/src/component/tags/form-tag-component.js b/frontend/src/component/tags/form-tag-component.js deleted file mode 100644 index a564920bae..0000000000 --- a/frontend/src/component/tags/form-tag-component.js +++ /dev/null @@ -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 ( - -
-
-

{errors.general}

- - this.setValue('type', v.target.value) - } - className={styles.select} - /> - - this.setValue('value', v.target.value) - } - className={styles.textfield} - /> -
- -
- -
-
- ); - } -} - -AddTagComponent.propTypes = { - tag: PropTypes.object.isRequired, - submit: PropTypes.func.isRequired, - history: PropTypes.object.isRequired, -}; - -export default AddTagComponent; diff --git a/frontend/src/component/tags/index.jsx b/frontend/src/component/tags/index.jsx deleted file mode 100644 index 35483b01fa..0000000000 --- a/frontend/src/component/tags/index.jsx +++ /dev/null @@ -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; diff --git a/frontend/src/component/user/UserProfile/UserProfileContent/UserProfileContent.styles.js b/frontend/src/component/user/UserProfile/UserProfileContent/UserProfileContent.styles.js index 21cceaad62..07446f1bff 100644 --- a/frontend/src/component/user/UserProfile/UserProfileContent/UserProfileContent.styles.js +++ b/frontend/src/component/user/UserProfile/UserProfileContent/UserProfileContent.styles.js @@ -3,7 +3,7 @@ import { makeStyles } from '@material-ui/styles'; export const useStyles = makeStyles(theme => ({ profile: { position: 'absolute', - zIndex: '5000', + zIndex: 5000, minWidth: '300px', right: 0, padding: '1.5rem', diff --git a/frontend/src/hooks/api/actions/useAddonsApi/useAddonsApi.ts b/frontend/src/hooks/api/actions/useAddonsApi/useAddonsApi.ts index bf2e8855b6..c26719bff0 100644 --- a/frontend/src/hooks/api/actions/useAddonsApi/useAddonsApi.ts +++ b/frontend/src/hooks/api/actions/useAddonsApi/useAddonsApi.ts @@ -1,4 +1,4 @@ -import { IAddons } from '../../../../interfaces/addons'; +import { IAddon } from '../../../../interfaces/addons'; import useAPI from '../useApi/useApi'; const useAddonsApi = () => { @@ -8,7 +8,7 @@ const useAddonsApi = () => { const URI = 'api/admin/addons'; - const createAddon = async (addonConfig: IAddons) => { + const createAddon = async (addonConfig: IAddon) => { const path = URI; const req = createRequest(path, { method: 'POST', @@ -38,7 +38,7 @@ const useAddonsApi = () => { } }; - const updateAddon = async (addonConfig: IAddons) => { + const updateAddon = async (addonConfig: IAddon) => { const path = `${URI}/${addonConfig.id}`; const req = createRequest(path, { method: 'PUT', diff --git a/frontend/src/hooks/api/actions/useApi/useApi.ts b/frontend/src/hooks/api/actions/useApi/useApi.ts index 8791587534..eb2157a278 100644 --- a/frontend/src/hooks/api/actions/useApi/useApi.ts +++ b/frontend/src/hooks/api/actions/useApi/useApi.ts @@ -163,14 +163,19 @@ const useAPI = ({ if (res.status > 399) { const response = await res.json(); - - if (response?.details?.length > 0) { + if (response?.details?.length > 0 && propagateErrors) { const error = response.details[0]; if (propagateErrors) { - throw new Error(error.message); + throw new Error(error.message || error.msg); } return error; } + + if (response?.length > 0 && propagateErrors) { + const error = response[0]; + throw new Error(error.message || error.msg); + } + if (propagateErrors) { throw new Error('Action could not be performed'); } diff --git a/frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts b/frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts index 9be6552ba2..364b319276 100644 --- a/frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts +++ b/frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts @@ -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 => { + const path = `api/admin/user-admin/search?q=${query}`; + + const req = createRequest(path, { method: 'GET' }); + + try { + const res = await makeRequest(req.caller, req.id); + + return res; + } catch (e) { + throw e; + } + }; + return { createProject, validateId, @@ -114,8 +162,11 @@ const useProjectApi = () => { deleteProject, addEnvironmentToProject, removeEnvironmentFromProject, + addUserToRole, + removeUserFromRole, errors, loading, + searchProjectUser, }; }; diff --git a/frontend/src/hooks/api/getters/httpErrorResponseHandler.ts b/frontend/src/hooks/api/getters/httpErrorResponseHandler.ts index b9a2f84db6..ac51696510 100644 --- a/frontend/src/hooks/api/getters/httpErrorResponseHandler.ts +++ b/frontend/src/hooks/api/getters/httpErrorResponseHandler.ts @@ -5,15 +5,15 @@ const handleErrorResponses = (target: string) => async (res: Response) => { ); // Try to resolve body, but don't rethrow res.json is not a function try { - // @ts-ignore + // @ts-expect-error error.info = await res.json(); } catch (e) { - // @ts-ignore + // @ts-expect-error error.info = {}; } - // @ts-ignore + // @ts-expect-error error.status = res.status; - // @ts-ignore + // @ts-expect-error error.statusText = res.statusText; throw error; } diff --git a/frontend/src/hooks/api/getters/useProjectAccess/useProjectAccess.ts b/frontend/src/hooks/api/getters/useProjectAccess/useProjectAccess.ts new file mode 100644 index 0000000000..ae62667811 --- /dev/null +++ b/frontend/src/hooks/api/getters/useProjectAccess/useProjectAccess.ts @@ -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( + 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; diff --git a/frontend/src/hooks/usePagination.ts b/frontend/src/hooks/usePagination.ts index f9ecea4380..366b118ee6 100644 --- a/frontend/src/hooks/usePagination.ts +++ b/frontend/src/hooks/usePagination.ts @@ -19,7 +19,7 @@ const usePagination = ( const result = paginate(dataToPaginate, limit); setPaginatedData(result); /* eslint-disable-next-line */ - }, [data, limit]); + }, [JSON.stringify(data), limit]); const nextPage = () => { if (pageIndex < paginatedData.length - 1) { diff --git a/frontend/src/hooks/useToast.tsx b/frontend/src/hooks/useToast.tsx index e4ce229faf..bdcade0916 100644 --- a/frontend/src/hooks/useToast.tsx +++ b/frontend/src/hooks/useToast.tsx @@ -20,7 +20,7 @@ interface IToastOptions { } const useToast = () => { - // @ts-ignore + // @ts-expect-error const { setToast } = useContext(UIContext); const hideToast = () => diff --git a/frontend/src/interfaces/addons.ts b/frontend/src/interfaces/addons.ts index 4ae31b449d..158fb3c18a 100644 --- a/frontend/src/interfaces/addons.ts +++ b/frontend/src/interfaces/addons.ts @@ -1,4 +1,6 @@ -export interface IAddons { +import { ITagType } from './tags'; + +export interface IAddon { id: number; provider: string; description: string; @@ -6,3 +8,32 @@ export interface IAddons { events: string[]; parameters: object; } + +export interface IAddonProvider { + description: string; + displayName: string; + documentationUrl: string; + events: string[]; + name: string; + parameters: IAddonProviderParams[]; + tagTypes: ITagType[]; +} + +export interface IAddonProviderParams { + name: string; + displayName: string; + type: string; + required: boolean; + sensitive: boolean; + placeholder?: string; + description?: string; +} + +export interface IAddonConfig { + description: string; + enabled: boolean; + events: string[]; + id: number; + parameters: Record; + provider: string; +} diff --git a/frontend/src/interfaces/params.ts b/frontend/src/interfaces/params.ts index a58af34e11..5cac9864d7 100644 --- a/frontend/src/interfaces/params.ts +++ b/frontend/src/interfaces/params.ts @@ -3,3 +3,7 @@ export interface IFeatureViewParams { featureId: string; activeTab: string; } + +export interface IProjectViewParams { + id: string; +} diff --git a/frontend/src/interfaces/role.ts b/frontend/src/interfaces/role.ts index efb2b62a59..479ad71c86 100644 --- a/frontend/src/interfaces/role.ts +++ b/frontend/src/interfaces/role.ts @@ -10,6 +10,7 @@ export interface IProjectRole { id: number; name: string; description: string; + type: string; } export default IRole; diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index 20d16423dc..c041b15c3c 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -9,6 +9,14 @@ export interface IUiConfig { versionInfo: IVersionInfo; links: ILinks[]; disablePasswordAuth?: boolean; + toast?: IProclamationToast +} + +export interface IProclamationToast { + message: string; + id: string; + severity: 'success' | 'info' | 'warning' | 'error'; + link: string; } export interface IFlags { diff --git a/frontend/src/page/addons/create.js b/frontend/src/page/addons/create.js deleted file mode 100644 index c9894d3484..0000000000 --- a/frontend/src/page/addons/create.js +++ /dev/null @@ -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 }) => ( - -); - -render.propTypes = { - match: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, -}; - -export default render; diff --git a/frontend/src/page/addons/edit.js b/frontend/src/page/addons/edit.js deleted file mode 100644 index ffe64e5387..0000000000 --- a/frontend/src/page/addons/edit.js +++ /dev/null @@ -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 }) => ( - -); - -render.propTypes = { - match: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, -}; - -export default render; diff --git a/frontend/src/page/addons/index.js b/frontend/src/page/addons/index.js deleted file mode 100644 index be6ab3f384..0000000000 --- a/frontend/src/page/addons/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import Addons from '../../component/addons'; -import PropTypes from 'prop-types'; - -const render = ({ history }) => ; - -render.propTypes = { - history: PropTypes.object.isRequired, -}; - -export default render; diff --git a/frontend/src/page/tag-types/create.js b/frontend/src/page/tag-types/create.js deleted file mode 100644 index c3cf82f127..0000000000 --- a/frontend/src/page/tag-types/create.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import AddTagType from '../../component/tag-types/create-tag-type-container'; -import PropTypes from 'prop-types'; - -const render = ({ history }) => ; - -render.propTypes = { - history: PropTypes.object.isRequired, -}; - -export default render; diff --git a/frontend/src/page/tag-types/edit.js b/frontend/src/page/tag-types/edit.js deleted file mode 100644 index 6c07fb920d..0000000000 --- a/frontend/src/page/tag-types/edit.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import EditTagType from './../../component/tag-types/edit-tag-type-container'; -import PropTypes from 'prop-types'; - -const render = ({ match: { params }, history }) => ( - -); -render.propTypes = { - match: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, -}; - -export default render; diff --git a/frontend/src/page/tag-types/index.js b/frontend/src/page/tag-types/index.js deleted file mode 100644 index 364d30e57e..0000000000 --- a/frontend/src/page/tag-types/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import TagTypes from '../../component/tag-types'; -import PropTypes from 'prop-types'; - -const render = ({ history }) => ; - -render.propTypes = { - history: PropTypes.object.isRequired, -}; - -export default render; diff --git a/frontend/src/page/tags/create.js b/frontend/src/page/tags/create.js deleted file mode 100644 index bb45eef335..0000000000 --- a/frontend/src/page/tags/create.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import AddTag from '../../component/tags/create-tag-container'; -import PropTypes from 'prop-types'; - -const render = ({ history }) => ; - -render.propTypes = { - history: PropTypes.object.isRequired, -}; - -export default render; diff --git a/frontend/src/page/tags/index.js b/frontend/src/page/tags/index.js deleted file mode 100644 index 26cc882ae9..0000000000 --- a/frontend/src/page/tags/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import Tags from '../../component/tags'; -import PropTypes from 'prop-types'; - -const render = ({ history }) => ; - -render.propTypes = { - history: PropTypes.object.isRequired, -}; - -export default render; diff --git a/frontend/src/store/addons/__testdata__/data.js b/frontend/src/store/addons/__testdata__/data.js deleted file mode 100644 index 5e18859d85..0000000000 --- a/frontend/src/store/addons/__testdata__/data.js +++ /dev/null @@ -1,69 +0,0 @@ -export const addonSimple = { - addons: [], - providers: [ - { - name: 'webhook', - displayName: 'Webhook', - parameters: [ - { - name: 'url', - displayName: 'Webhook URL', - type: 'string', - }, - { - name: 'unleashUrl', - displayName: 'Unleash Admin UI url', - type: 'text', - }, - { - name: 'bodyTemplate', - displayName: 'Body template', - description: 'You may format the body using a mustache template.', - type: 'text', - }, - ], - events: ['feature-created', 'feature-updated', 'feature-archived', 'feature-revived'], - }, - ], -}; - -export const addonConfig = { - id: 1, - provider: 'webhook', - enabled: true, - description: null, - parameters: { - url: 'http://localhost:4242/webhook', - bodyTemplate: "{'name': '{{event.data.name}}' }", - }, - events: ['feature-updated', 'feature-created'], -}; - -export const addonsWithConfig = { - addons: [addonConfig], - providers: [ - { - name: 'webhook', - displayName: 'Webhook', - parameters: [ - { - name: 'url', - displayName: 'Webhook URL', - type: 'string', - }, - { - name: 'unleashUrl', - displayName: 'Unleash Admin UI url', - type: 'text', - }, - { - name: 'bodyTemplate', - displayName: 'Body template', - description: 'You may format the body using a mustache template.', - type: 'text', - }, - ], - events: ['feature-created', 'feature-updated', 'feature-archived', 'feature-revived'], - }, - ], -}; diff --git a/frontend/src/store/addons/__tests__/__snapshots__/addons-store.test.js.snap b/frontend/src/store/addons/__tests__/__snapshots__/addons-store.test.js.snap deleted file mode 100644 index a417521d15..0000000000 --- a/frontend/src/store/addons/__tests__/__snapshots__/addons-store.test.js.snap +++ /dev/null @@ -1,189 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should add addon-config 1`] = ` -Object { - "addons": Array [ - Object { - "description": null, - "enabled": true, - "events": Array [ - "feature-updated", - "feature-created", - ], - "id": 1, - "parameters": Object { - "bodyTemplate": "{'name': '{{event.data.name}}' }", - "url": "http://localhost:4242/webhook", - }, - "provider": "webhook", - }, - ], - "providers": Array [ - Object { - "displayName": "Webhook", - "events": Array [ - "feature-created", - "feature-updated", - "feature-archived", - "feature-revived", - ], - "name": "webhook", - "parameters": Array [ - Object { - "displayName": "Webhook URL", - "name": "url", - "type": "string", - }, - Object { - "displayName": "Unleash Admin UI url", - "name": "unleashUrl", - "type": "text", - }, - Object { - "description": "You may format the body using a mustache template.", - "displayName": "Body template", - "name": "bodyTemplate", - "type": "text", - }, - ], - }, - ], -} -`; - -exports[`should be default state 1`] = ` -Object { - "addons": Array [], - "providers": Array [], -} -`; - -exports[`should be merged state all 1`] = ` -Object { - "addons": Array [], - "providers": Array [ - Object { - "displayName": "Webhook", - "events": Array [ - "feature-created", - "feature-updated", - "feature-archived", - "feature-revived", - ], - "name": "webhook", - "parameters": Array [ - Object { - "displayName": "Webhook URL", - "name": "url", - "type": "string", - }, - Object { - "displayName": "Unleash Admin UI url", - "name": "unleashUrl", - "type": "text", - }, - Object { - "description": "You may format the body using a mustache template.", - "displayName": "Body template", - "name": "bodyTemplate", - "type": "text", - }, - ], - }, - ], -} -`; - -exports[`should clear addon-config on logout 1`] = ` -Object { - "addons": Array [], - "providers": Array [], -} -`; - -exports[`should remove addon-config 1`] = ` -Object { - "addons": Array [], - "providers": Array [ - Object { - "displayName": "Webhook", - "events": Array [ - "feature-created", - "feature-updated", - "feature-archived", - "feature-revived", - ], - "name": "webhook", - "parameters": Array [ - Object { - "displayName": "Webhook URL", - "name": "url", - "type": "string", - }, - Object { - "displayName": "Unleash Admin UI url", - "name": "unleashUrl", - "type": "text", - }, - Object { - "description": "You may format the body using a mustache template.", - "displayName": "Body template", - "name": "bodyTemplate", - "type": "text", - }, - ], - }, - ], -} -`; - -exports[`should update addon-config 1`] = ` -Object { - "addons": Array [ - Object { - "description": "new desc", - "enabled": false, - "events": Array [ - "feature-updated", - "feature-created", - ], - "id": 1, - "parameters": Object { - "bodyTemplate": "{'name': '{{event.data.name}}' }", - "url": "http://localhost:4242/webhook", - }, - "provider": "webhook", - }, - ], - "providers": Array [ - Object { - "displayName": "Webhook", - "events": Array [ - "feature-created", - "feature-updated", - "feature-archived", - "feature-revived", - ], - "name": "webhook", - "parameters": Array [ - Object { - "displayName": "Webhook URL", - "name": "url", - "type": "string", - }, - Object { - "displayName": "Unleash Admin UI url", - "name": "unleashUrl", - "type": "text", - }, - Object { - "description": "You may format the body using a mustache template.", - "displayName": "Body template", - "name": "bodyTemplate", - "type": "text", - }, - ], - }, - ], -} -`; diff --git a/frontend/src/store/addons/__tests__/addons-actions.test.js b/frontend/src/store/addons/__tests__/addons-actions.test.js deleted file mode 100644 index bfea9ce49a..0000000000 --- a/frontend/src/store/addons/__tests__/addons-actions.test.js +++ /dev/null @@ -1,113 +0,0 @@ -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import fetchMock from 'fetch-mock'; - -import { - RECEIVE_ADDON_CONFIG, - ERROR_RECEIVE_ADDON_CONFIG, - REMOVE_ADDON_CONFIG, - UPDATE_ADDON_CONFIG, - ADD_ADDON_CONFIG, - fetchAddons, - removeAddon, - updateAddon, - createAddon, -} from '../actions'; - -const middlewares = [thunk]; -const mockStore = configureMockStore(middlewares); - -afterEach(() => { - fetchMock.restore(); -}); - -test('creates RECEIVE_ADDON_CONFIG when fetching addons has been done', () => { - fetchMock.getOnce('api/admin/addons', { - body: { addons: { providers: [{ name: 'webhook' }] } }, - headers: { 'content-type': 'application/json' }, - }); - - const expectedActions = [{ type: RECEIVE_ADDON_CONFIG, value: { addons: { providers: [{ name: 'webhook' }] } } }]; - const store = mockStore({ addons: [] }); - - return store.dispatch(fetchAddons()).then(() => { - // return of async actions - expect(store.getActions()).toEqual(expectedActions); - }); -}); - -test('creates RECEIVE_ADDON_CONFIG_ when fetching addons has been done', () => { - fetchMock.getOnce('api/admin/addons', { - body: { message: 'Server error' }, - headers: { 'content-type': 'application/json' }, - status: 500, - }); - - const store = mockStore({ addons: [] }); - - return store.dispatch(fetchAddons()).catch(e => { - // return of async actions - expect(store.getActions()[0].type).toEqual(ERROR_RECEIVE_ADDON_CONFIG); - expect(e.message).toEqual('Unexpected exception when talking to unleash-api'); - }); -}); - -test('creates REMOVE_ADDON_CONFIG when delete addon has been done', () => { - const addon = { - id: 1, - provider: 'webhook', - }; - - fetchMock.deleteOnce('api/admin/addons/1', { - status: 200, - }); - - const expectedActions = [{ type: REMOVE_ADDON_CONFIG, value: addon }]; - const store = mockStore({ addons: [] }); - - return store.dispatch(removeAddon(addon)).then(() => { - // return of async actions - expect(store.getActions()).toEqual(expectedActions); - }); -}); - -test('creates UPDATE_ADDON_CONFIG when delete addon has been done', () => { - const addon = { - id: 1, - provider: 'webhook', - }; - - fetchMock.putOnce('api/admin/addons/1', { - headers: { 'content-type': 'application/json' }, - status: 200, - body: addon, - }); - - const expectedActions = [{ type: UPDATE_ADDON_CONFIG, value: addon }]; - const store = mockStore({ addons: [] }); - - return store.dispatch(updateAddon(addon)).then(() => { - // return of async actions - expect(store.getActions()).toEqual(expectedActions); - }); -}); - -test('creates ADD_ADDON_CONFIG when delete addon has been done', () => { - const addon = { - provider: 'webhook', - }; - - fetchMock.postOnce('api/admin/addons', { - headers: { 'content-type': 'application/json' }, - status: 200, - body: addon, - }); - - const expectedActions = [{ type: ADD_ADDON_CONFIG, value: addon }]; - const store = mockStore({ addons: [] }); - - return store.dispatch(createAddon(addon)).then(() => { - // return of async actions - expect(store.getActions()).toEqual(expectedActions); - }); -}); diff --git a/frontend/src/store/addons/__tests__/addons-store.test.js b/frontend/src/store/addons/__tests__/addons-store.test.js deleted file mode 100644 index 9bdabc0199..0000000000 --- a/frontend/src/store/addons/__tests__/addons-store.test.js +++ /dev/null @@ -1,54 +0,0 @@ -import reducer from '../index'; -import { RECEIVE_ADDON_CONFIG, ADD_ADDON_CONFIG, REMOVE_ADDON_CONFIG, UPDATE_ADDON_CONFIG } from '../actions'; -import { addonSimple, addonsWithConfig, addonConfig } from '../__testdata__/data'; -import { USER_LOGOUT } from '../../user/actions'; - -test('should be default state', () => { - const state = reducer(undefined, {}); - expect(state.toJS()).toMatchSnapshot(); -}); - -test('should be merged state all', () => { - const state = reducer(undefined, { type: RECEIVE_ADDON_CONFIG, value: addonSimple }); - expect(state.toJS()).toMatchSnapshot(); -}); - -test('should add addon-config', () => { - let state = reducer(undefined, { type: RECEIVE_ADDON_CONFIG, value: addonSimple }); - state = reducer(state, { type: ADD_ADDON_CONFIG, value: addonConfig }); - - const data = state.toJS(); - expect(data).toMatchSnapshot(); - expect(data.addons.length).toBe(1); -}); - -test('should remove addon-config', () => { - let state = reducer(undefined, { type: RECEIVE_ADDON_CONFIG, value: addonsWithConfig }); - state = reducer(state, { type: REMOVE_ADDON_CONFIG, value: addonConfig }); - - const data = state.toJS(); - expect(data).toMatchSnapshot(); - expect(data.addons.length).toBe(0); -}); - -test('should update addon-config', () => { - const updateAdddonConfig = { ...addonConfig, description: 'new desc', enabled: false }; - - let state = reducer(undefined, { type: RECEIVE_ADDON_CONFIG, value: addonsWithConfig }); - state = reducer(state, { type: UPDATE_ADDON_CONFIG, value: updateAdddonConfig }); - - const data = state.toJS(); - expect(data).toMatchSnapshot(); - expect(data.addons.length).toBe(1); - expect(data.addons[0].description).toBe('new desc'); -}); - -test('should clear addon-config on logout', () => { - let state = reducer(undefined, { type: RECEIVE_ADDON_CONFIG, value: addonsWithConfig }); - state = reducer(state, { type: USER_LOGOUT }); - - const data = state.toJS(); - expect(data).toMatchSnapshot(); - expect(data.addons.length).toBe(0); - expect(data.providers.length).toBe(0); -}); diff --git a/frontend/src/store/addons/actions.js b/frontend/src/store/addons/actions.js deleted file mode 100644 index 2ecc3b2b23..0000000000 --- a/frontend/src/store/addons/actions.js +++ /dev/null @@ -1,51 +0,0 @@ -import api from './api'; -import { dispatchError } from '../util'; - -export const RECEIVE_ADDON_CONFIG = 'RECEIVE_ADDON_CONFIG'; -export const ERROR_RECEIVE_ADDON_CONFIG = 'ERROR_RECEIVE_ADDON_CONFIG'; -export const REMOVE_ADDON_CONFIG = 'REMOVE_ADDON_CONFIG'; -export const ERROR_REMOVING_ADDON_CONFIG = 'ERROR_REMOVING_ADDON_CONFIG'; -export const ADD_ADDON_CONFIG = 'ADD_ADDON_CONFIG'; -export const ERROR_ADD_ADDON_CONFIG = 'ERROR_ADD_ADDON_CONFIG'; -export const UPDATE_ADDON_CONFIG = 'UPDATE_ADDON_CONFIG'; -export const ERROR_UPDATE_ADDON_CONFIG = 'ERROR_UPDATE_ADDON_CONFIG'; - -// const receiveAddonConfig = value => ({ type: RECEIVE_ADDON_CONFIG, value }); -const addAddonConfig = value => ({ type: ADD_ADDON_CONFIG, value }); -const updateAdddonConfig = value => ({ type: UPDATE_ADDON_CONFIG, value }); -const removeAddonconfig = value => ({ type: REMOVE_ADDON_CONFIG, value }); - -const success = (dispatch, type) => value => dispatch({ type, value }); - -export function fetchAddons() { - return dispatch => - api - .fetchAll() - .then(success(dispatch, RECEIVE_ADDON_CONFIG)) - .catch(dispatchError(dispatch, ERROR_RECEIVE_ADDON_CONFIG)); -} - -export function removeAddon(addon) { - return dispatch => - api - .remove(addon) - .then(() => dispatch(removeAddonconfig(addon))) - .catch(dispatchError(dispatch, ERROR_REMOVING_ADDON_CONFIG)); -} - -export function createAddon(addon) { - return dispatch => - api - .create(addon) - .then(res => res.json()) - .then(value => dispatch(addAddonConfig(value))) - .catch(dispatchError(dispatch, ERROR_ADD_ADDON_CONFIG)); -} - -export function updateAddon(addon) { - return dispatch => - api - .update(addon) - .then(() => dispatch(updateAdddonConfig(addon))) - .catch(dispatchError(dispatch, ERROR_UPDATE_ADDON_CONFIG)); -} diff --git a/frontend/src/store/addons/api.js b/frontend/src/store/addons/api.js deleted file mode 100644 index 72decf1494..0000000000 --- a/frontend/src/store/addons/api.js +++ /dev/null @@ -1,44 +0,0 @@ -import { formatApiPath } from '../../utils/format-path'; -import { throwIfNotSuccess, headers } from '../api-helper'; - -const URI = formatApiPath(`api/admin/addons`); - -function fetchAll() { - return fetch(URI, { credentials: 'include' }) - .then(throwIfNotSuccess) - .then(response => response.json()); -} - -function create(addonConfig) { - return fetch(URI, { - method: 'POST', - headers, - body: JSON.stringify(addonConfig), - credentials: 'include', - }).then(throwIfNotSuccess); -} - -function update(addonConfig) { - return fetch(`${URI}/${addonConfig.id}`, { - method: 'PUT', - headers, - body: JSON.stringify(addonConfig), - credentials: 'include', - }).then(throwIfNotSuccess); -} - -function remove(addonConfig) { - return fetch(`${URI}/${addonConfig.id}`, { - method: 'DELETE', - headers, - credentials: 'include', - }).then(throwIfNotSuccess); -} - -const api = { - fetchAll, - create, - update, - remove, -}; -export default api; diff --git a/frontend/src/store/addons/index.js b/frontend/src/store/addons/index.js deleted file mode 100644 index ab31635f7e..0000000000 --- a/frontend/src/store/addons/index.js +++ /dev/null @@ -1,33 +0,0 @@ -import { Map as $Map, List, fromJS } from 'immutable'; -import { RECEIVE_ADDON_CONFIG, ADD_ADDON_CONFIG, REMOVE_ADDON_CONFIG, UPDATE_ADDON_CONFIG } from './actions'; -import { USER_LOGOUT, USER_LOGIN } from '../user/actions'; - -function getInitState() { - return new $Map({ - providers: new List(), - addons: new List(), - }); -} - -const strategies = (state = getInitState(), action) => { - switch (action.type) { - case RECEIVE_ADDON_CONFIG: - return fromJS(action.value); - case ADD_ADDON_CONFIG: { - return state.update('addons', arr => arr.push(fromJS(action.value))); - } - case REMOVE_ADDON_CONFIG: - return state.update('addons', arr => arr.filter(a => a.get('id') !== action.value.id)); - case UPDATE_ADDON_CONFIG: { - const index = state.get('addons').findIndex(item => item.get('id') === action.value.id); - return state.setIn(['addons', index], fromJS(action.value)); - } - case USER_LOGOUT: - case USER_LOGIN: - return getInitState(); - default: - return state; - } -}; - -export default strategies; diff --git a/frontend/src/store/context/actions.js b/frontend/src/store/context/actions.js deleted file mode 100644 index 93d4386c1a..0000000000 --- a/frontend/src/store/context/actions.js +++ /dev/null @@ -1,58 +0,0 @@ -import api from './api'; -import { dispatchError } from '../util'; - -export const RECEIVE_CONTEXT = 'RECEIVE_CONTEXT'; -export const ERROR_RECEIVE_CONTEXT = 'ERROR_RECEIVE_CONTEXT'; -export const REMOVE_CONTEXT = 'REMOVE_CONTEXT'; -export const ERROR_REMOVING_CONTEXT = 'ERROR_REMOVING_CONTEXT'; -export const ADD_CONTEXT_FIELD = 'ADD_CONTEXT_FIELD'; -export const ERROR_ADD_CONTEXT_FIELD = 'ERROR_ADD_CONTEXT_FIELD'; -export const UPDATE_CONTEXT_FIELD = 'UPDATE_CONTEXT_FIELD'; -export const ERROR_UPDATE_CONTEXT_FIELD = 'ERROR_UPDATE_CONTEXT_FIELD'; - -export const receiveContext = value => ({ type: RECEIVE_CONTEXT, value }); -const addContextField = context => ({ type: ADD_CONTEXT_FIELD, context }); -const upContextField = context => ({ type: UPDATE_CONTEXT_FIELD, context }); -const createRemoveContext = context => ({ type: REMOVE_CONTEXT, context }); - -export function fetchContext() { - return dispatch => - api - .fetchAll() - .then(json => { - json.sort((a, b) => a.sortOrder - b.sortOrder); - dispatch(receiveContext(json)); - }) - .catch(dispatchError(dispatch, ERROR_RECEIVE_CONTEXT)); -} - -export function removeContextField(context) { - return dispatch => - api - .remove(context) - .then(() => dispatch(createRemoveContext(context))) - .catch(dispatchError(dispatch, ERROR_REMOVING_CONTEXT)); -} - -export function createContextField(context) { - return dispatch => - api - .create(context) - .then(() => dispatch(addContextField(context))) - .catch(e => { - dispatchError(dispatch, ERROR_ADD_CONTEXT_FIELD); - throw e; - }); -} - -export function updateContextField(context) { - return dispatch => - api - .update(context) - .then(() => dispatch(upContextField(context))) - .catch(dispatchError(dispatch, ERROR_UPDATE_CONTEXT_FIELD)); -} - -export function validateName(name) { - return api.validate({ name }); -} diff --git a/frontend/src/store/context/api.js b/frontend/src/store/context/api.js deleted file mode 100644 index 00423dbdd0..0000000000 --- a/frontend/src/store/context/api.js +++ /dev/null @@ -1,53 +0,0 @@ -import { formatApiPath } from '../../utils/format-path'; -import { throwIfNotSuccess, headers } from '../api-helper'; - -const URI = formatApiPath('api/admin/context'); - -function fetchAll() { - return fetch(URI, { credentials: 'include' }) - .then(throwIfNotSuccess) - .then(response => response.json()); -} - -function create(contextField) { - return fetch(URI, { - method: 'POST', - headers, - body: JSON.stringify(contextField), - credentials: 'include', - }).then(throwIfNotSuccess); -} - -function update(contextField) { - return fetch(`${URI}/${contextField.name}`, { - method: 'PUT', - headers, - body: JSON.stringify(contextField), - credentials: 'include', - }).then(throwIfNotSuccess); -} - -function remove(contextField) { - return fetch(`${URI}/${contextField.name}`, { - method: 'DELETE', - headers, - credentials: 'include', - }).then(throwIfNotSuccess); -} - -function validate(name) { - return fetch(`${URI}/validate`, { - method: 'POST', - headers, - credentials: 'include', - body: JSON.stringify(name), - }).then(throwIfNotSuccess); -} - -export default { - fetchAll, - create, - update, - remove, - validate, -}; diff --git a/frontend/src/store/context/index.js b/frontend/src/store/context/index.js deleted file mode 100644 index 2133b39bdf..0000000000 --- a/frontend/src/store/context/index.js +++ /dev/null @@ -1,46 +0,0 @@ -import { List } from 'immutable'; -import { - RECEIVE_CONTEXT, - REMOVE_CONTEXT, - ADD_CONTEXT_FIELD, - UPDATE_CONTEXT_FIELD, -} from './actions'; -import { USER_LOGOUT, USER_LOGIN } from '../user/actions'; - -const DEFAULT_CONTEXT_FIELDS = [ - { name: 'environment', initial: true }, - { name: 'userId', initial: true }, - { name: 'appName', initial: true }, -]; - -function getInitState() { - return new List(DEFAULT_CONTEXT_FIELDS); -} - -const context = (state = getInitState(), action) => { - switch (action.type) { - case RECEIVE_CONTEXT: - return new List(action.value); - case REMOVE_CONTEXT: - const index = state.findIndex( - item => item.name === action.context.name - ); - - return state.remove(index); - case ADD_CONTEXT_FIELD: - return state.push(action.context); - case UPDATE_CONTEXT_FIELD: { - const index = state.findIndex( - item => item.name === action.context.name - ); - return state.set(index, action.context); - } - case USER_LOGOUT: - case USER_LOGIN: - return getInitState(); - default: - return state; - } -}; - -export default context; diff --git a/frontend/src/store/e-admin-invoice/actions.js b/frontend/src/store/e-admin-invoice/actions.js deleted file mode 100644 index 4a7a2cac8b..0000000000 --- a/frontend/src/store/e-admin-invoice/actions.js +++ /dev/null @@ -1,20 +0,0 @@ -import api from './api'; -import { dispatchError } from '../util'; -export const RECEIVE_INVOICES = 'RECEIVE_INVOICES'; -export const ERROR_FETCH_INVOICES = 'ERROR_FETCH_INVOICES'; - -const debug = require('debug')('unleash:e-admin-invoice-actions'); - -export function fetchInvoices() { - debug('Start fetching invoices for hosted customer'); - return dispatch => - api - .fetchAll() - .then(value => - dispatch({ - type: RECEIVE_INVOICES, - invoices: value.invoices, - }) - ) - .catch(dispatchError(dispatch, ERROR_FETCH_INVOICES)); -} \ No newline at end of file diff --git a/frontend/src/store/e-admin-invoice/api.js b/frontend/src/store/e-admin-invoice/api.js deleted file mode 100644 index 0918ad1d68..0000000000 --- a/frontend/src/store/e-admin-invoice/api.js +++ /dev/null @@ -1,14 +0,0 @@ -import { formatApiPath } from '../../utils/format-path'; -import { throwIfNotSuccess, headers } from '../api-helper'; - -const URI = formatApiPath('api/admin/invoices'); - -function fetchAll() { - return fetch(URI, { headers, credentials: 'include' }) - .then(throwIfNotSuccess) - .then(response => response.json()); -} - -export default { - fetchAll -}; diff --git a/frontend/src/store/e-admin-invoice/index.js b/frontend/src/store/e-admin-invoice/index.js deleted file mode 100644 index 9def0dc310..0000000000 --- a/frontend/src/store/e-admin-invoice/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import { List } from 'immutable'; -import { RECEIVE_INVOICES } from './actions'; - -const store = (state = new List(), action) => { - switch (action.type) { - case RECEIVE_INVOICES: - return new List(action.invoices); - default: - return state; - } -}; - -export default store; diff --git a/frontend/src/store/error/index.js b/frontend/src/store/error/index.js index 4544163bb0..ed4554436f 100644 --- a/frontend/src/store/error/index.js +++ b/frontend/src/store/error/index.js @@ -16,23 +16,12 @@ import { UPDATE_STRATEGY_SUCCESS, } from '../strategy/actions'; -import { - ERROR_ADD_CONTEXT_FIELD, - ERROR_UPDATE_CONTEXT_FIELD, -} from '../context/actions'; - import { ERROR_REMOVING_PROJECT, ERROR_ADD_PROJECT, ERROR_UPDATE_PROJECT, } from '../project/actions'; -import { - ERROR_ADD_ADDON_CONFIG, - ERROR_UPDATE_ADDON_CONFIG, - ERROR_REMOVING_ADDON_CONFIG, -} from '../addons/actions'; - import { UPDATE_APPLICATION_FIELD } from '../application/actions'; import { FORBIDDEN } from '../util'; @@ -62,13 +51,8 @@ const strategies = (state = getInitState(), action) => { case ERROR_UPDATING_STRATEGY: case ERROR_CREATING_STRATEGY: case ERROR_RECEIVE_STRATEGIES: - case ERROR_ADD_CONTEXT_FIELD: - case ERROR_UPDATE_CONTEXT_FIELD: case ERROR_REMOVING_PROJECT: case ERROR_UPDATE_PROJECT: - case ERROR_ADD_ADDON_CONFIG: - case ERROR_UPDATE_ADDON_CONFIG: - case ERROR_REMOVING_ADDON_CONFIG: case ERROR_ADD_PROJECT: return addErrorIfNotAlreadyInList(state, action.error.message); case FORBIDDEN: diff --git a/frontend/src/store/feature-metrics/actions.js b/frontend/src/store/feature-metrics/actions.js deleted file mode 100644 index de3eee76ef..0000000000 --- a/frontend/src/store/feature-metrics/actions.js +++ /dev/null @@ -1,54 +0,0 @@ -import api from './api'; - -export const START_FETCH_FEATURE_METRICS = 'START_FETCH_FEATURE_METRICS'; -export const RECEIVE_FEATURE_METRICS = 'RECEIVE_FEATURE_METRICS'; -export const ERROR_FETCH_FEATURE_METRICS = 'ERROR_FETCH_FEATURE_METRICS'; - -export const START_FETCH_SEEN_APP = 'START_FETCH_SEEN_APP'; -export const RECEIVE_SEEN_APPS = 'RECEIVE_SEEN_APPS'; -export const ERROR_FETCH_SEEN_APP = 'ERROR_FETCH_SEEN_APP'; - -function receiveFeatureMetrics(json) { - return { - type: RECEIVE_FEATURE_METRICS, - value: json, - receivedAt: Date.now(), - }; -} - -function receiveSeenApps(json) { - return { - type: RECEIVE_SEEN_APPS, - value: json, - receivedAt: Date.now(), - }; -} - -function dispatchAndThrow(dispatch, type) { - return error => { - dispatch({ type, error, receivedAt: Date.now() }); - // throw error; - }; -} - -export function fetchFeatureMetrics() { - return dispatch => { - dispatch({ type: START_FETCH_FEATURE_METRICS }); - - return api - .fetchFeatureMetrics() - .then(json => dispatch(receiveFeatureMetrics(json))) - .catch(dispatchAndThrow(dispatch, ERROR_FETCH_FEATURE_METRICS)); - }; -} - -export function fetchSeenApps() { - return dispatch => { - dispatch({ type: START_FETCH_SEEN_APP }); - - return api - .fetchSeenApps() - .then(json => dispatch(receiveSeenApps(json))) - .catch(dispatchAndThrow(dispatch, ERROR_FETCH_SEEN_APP)); - }; -} diff --git a/frontend/src/store/feature-metrics/api.js b/frontend/src/store/feature-metrics/api.js deleted file mode 100644 index 3fe10dd7ca..0000000000 --- a/frontend/src/store/feature-metrics/api.js +++ /dev/null @@ -1,23 +0,0 @@ -import { formatApiPath } from '../../utils/format-path'; -import { throwIfNotSuccess } from '../api-helper'; - -const URI = formatApiPath('api/admin/metrics/feature-toggles'); - -function fetchFeatureMetrics() { - return fetch(URI, { credentials: 'include' }) - .then(throwIfNotSuccess) - .then(response => response.json()); -} - -const seenURI = formatApiPath('api/admin/metrics/seen-apps'); - -function fetchSeenApps() { - return fetch(seenURI, { credentials: 'include' }) - .then(throwIfNotSuccess) - .then(response => response.json()); -} - -export default { - fetchFeatureMetrics, - fetchSeenApps, -}; diff --git a/frontend/src/store/feature-metrics/index.js b/frontend/src/store/feature-metrics/index.js deleted file mode 100644 index 5a51c56d04..0000000000 --- a/frontend/src/store/feature-metrics/index.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Map as $Map, fromJS } from 'immutable'; - -import { RECEIVE_FEATURE_METRICS, RECEIVE_SEEN_APPS } from './actions'; - -const metrics = ( - state = fromJS({ lastHour: {}, lastMinute: {}, seenApps: {} }), - action -) => { - switch (action.type) { - case RECEIVE_SEEN_APPS: - return state.set('seenApps', new $Map(action.value)); - case RECEIVE_FEATURE_METRICS: - return state.withMutations(ctx => { - ctx.set('lastHour', new $Map(action.value.lastHour)); - ctx.set('lastMinute', new $Map(action.value.lastMinute)); - return ctx; - }); - default: - return state; - } -}; - -export default metrics; diff --git a/frontend/src/store/feature-tags/actions.js b/frontend/src/store/feature-tags/actions.js deleted file mode 100644 index 87e82b4399..0000000000 --- a/frontend/src/store/feature-tags/actions.js +++ /dev/null @@ -1,50 +0,0 @@ -import api from './api'; -import { dispatchError } from '../util'; - -export const TAG_FEATURE_TOGGLE = 'TAG_FEATURE_TOGGLE'; -export const UNTAG_FEATURE_TOGGLE = 'UNTAG_FEATURE_TOGGLE'; -export const START_TAG_FEATURE_TOGGLE = 'START_TAG_FEATURE_TOGGLE'; -export const START_UNTAG_FEATURE_TOGGLE = 'START_UNTAG_FEATURE_TOGGLE'; -export const ERROR_TAG_FEATURE_TOGGLE = 'ERROR_TAG_FEATURE_TOGGLE'; -export const ERROR_UNTAG_FEATURE_TOGGLE = 'ERROR_UNTAG_FEATURE_TOGGLE'; -export const START_FETCH_FEATURE_TOGGLE_TAGS = 'START_FETCH_FEATURE_TOGGLE_TAGS'; -export const RECEIVE_FEATURE_TOGGLE_TAGS = 'RECEIVE_FEATURE_TOGGLE_TAGS'; -export const ERROR_FETCH_FEATURE_TOGGLE_TAGS = 'ERROR_FETCH_FEATURE_TOGGLE_TAGS'; - -function receiveFeatureToggleTags(json) { - return { - type: RECEIVE_FEATURE_TOGGLE_TAGS, - value: json, - receivedAt: Date.now(), - }; -} - -export function tagFeature(featureToggle, tag) { - return dispatch => { - dispatch({ type: START_TAG_FEATURE_TOGGLE }); - return api - .tagFeature(featureToggle, tag) - .then(json => dispatch({ type: TAG_FEATURE_TOGGLE, featureToggle, tag: json })) - .catch(dispatchError(dispatch, ERROR_TAG_FEATURE_TOGGLE)); - }; -} - -export function untagFeature(featureToggle, tag) { - return dispatch => { - dispatch({ type: START_UNTAG_FEATURE_TOGGLE }); - return api - .untagFeature(featureToggle, tag) - .then(() => dispatch({ type: UNTAG_FEATURE_TOGGLE, featureToggle, tag })) - .catch(dispatchError(dispatch, ERROR_UNTAG_FEATURE_TOGGLE)); - }; -} - -export function fetchTags(featureToggle) { - return dispatch => { - dispatch({ type: START_FETCH_FEATURE_TOGGLE_TAGS }); - return api - .fetchFeatureToggleTags(featureToggle) - .then(json => dispatch(receiveFeatureToggleTags(json))) - .catch(dispatchError(dispatch, ERROR_FETCH_FEATURE_TOGGLE_TAGS)); - }; -} diff --git a/frontend/src/store/feature-tags/api.js b/frontend/src/store/feature-tags/api.js deleted file mode 100644 index 5b0574408d..0000000000 --- a/frontend/src/store/feature-tags/api.js +++ /dev/null @@ -1,44 +0,0 @@ -import { formatApiPath } from '../../utils/format-path'; -import { throwIfNotSuccess, headers } from '../api-helper'; - -const URI = formatApiPath('api/admin/features'); - -function tagFeature(featureToggle, tag) { - return fetch(`${URI}/${featureToggle}/tags`, { - method: 'POST', - headers, - credentials: 'include', - body: JSON.stringify(tag), - }) - .then(throwIfNotSuccess) - .then(response => response.json()); -} - -function untagFeature(featureToggle, tag) { - return fetch( - `${URI}/${featureToggle}/tags/${tag.type}/${encodeURIComponent( - tag.value - )}`, - { - method: 'DELETE', - headers, - credentials: 'include', - } - ).then(throwIfNotSuccess); -} - -function fetchFeatureToggleTags(featureToggle) { - return fetch(`${URI}/${featureToggle}/tags`, { - method: 'GET', - headers, - credentials: 'include', - }) - .then(throwIfNotSuccess) - .then(response => response.json()); -} - -export default { - tagFeature, - untagFeature, - fetchFeatureToggleTags, -}; diff --git a/frontend/src/store/feature-tags/index.js b/frontend/src/store/feature-tags/index.js deleted file mode 100644 index dcc6383ef4..0000000000 --- a/frontend/src/store/feature-tags/index.js +++ /dev/null @@ -1,25 +0,0 @@ -import { List, Map as $MAP } from 'immutable'; -import { RECEIVE_FEATURE_TOGGLE_TAGS, TAG_FEATURE_TOGGLE, UNTAG_FEATURE_TOGGLE } from './actions'; - -function getInitState() { - return new List(); -} - -const featureTags = (state = getInitState(), action) => { - switch (action.type) { - case RECEIVE_FEATURE_TOGGLE_TAGS: - if (action.value) { - return new List(action.value.tags); - } else { - return getInitState(); - } - case TAG_FEATURE_TOGGLE: - return state.push(new $MAP(action.tag)); - case UNTAG_FEATURE_TOGGLE: - return state.remove(state.findIndex(t => t.value === action.tag.value && t.type === action.tag.type)); - default: - return state; - } -}; - -export default featureTags; diff --git a/frontend/src/store/feature-type/actions.js b/frontend/src/store/feature-type/actions.js deleted file mode 100644 index 7eed119f9f..0000000000 --- a/frontend/src/store/feature-type/actions.js +++ /dev/null @@ -1,18 +0,0 @@ -import api from './api'; -import { dispatchError } from '../util'; - -export const RECEIVE_FEATURE_TYPES = 'RECEIVE_FEATURE_TYPES'; -export const ERROR_RECEIVE_FEATURE_TYPES = 'ERROR_RECEIVE_FEATURE_TYPES'; - -export const receiveFeatureTypes = value => ({ - type: RECEIVE_FEATURE_TYPES, - value, -}); - -export function fetchFeatureTypes() { - return dispatch => - api - .fetchAll() - .then(json => dispatch(receiveFeatureTypes(json.types))) - .catch(dispatchError(dispatch, ERROR_RECEIVE_FEATURE_TYPES)); -} diff --git a/frontend/src/store/feature-type/api.js b/frontend/src/store/feature-type/api.js deleted file mode 100644 index 37074bfc1d..0000000000 --- a/frontend/src/store/feature-type/api.js +++ /dev/null @@ -1,14 +0,0 @@ -import { formatApiPath } from '../../utils/format-path'; -import { throwIfNotSuccess } from '../api-helper'; - -const URI = formatApiPath('api/admin/feature-types'); - -function fetchAll() { - return fetch(URI, { credentials: 'include' }) - .then(throwIfNotSuccess) - .then(response => response.json()); -} - -export default { - fetchAll, -}; diff --git a/frontend/src/store/feature-type/index.js b/frontend/src/store/feature-type/index.js deleted file mode 100644 index f31efacd7a..0000000000 --- a/frontend/src/store/feature-type/index.js +++ /dev/null @@ -1,21 +0,0 @@ -import { List } from 'immutable'; -import { RECEIVE_FEATURE_TYPES } from './actions'; - -const DEFAULT_FEATURE_TYPES = [ - { id: 'release', name: 'Release', initial: true }, -]; - -function getInitState() { - return new List(DEFAULT_FEATURE_TYPES); -} - -const strategies = (state = getInitState(), action) => { - switch (action.type) { - case RECEIVE_FEATURE_TYPES: - return new List(action.value); - default: - return state; - } -}; - -export default strategies; diff --git a/frontend/src/store/feedback/actions.js b/frontend/src/store/feedback/actions.js deleted file mode 100644 index 03c335bd73..0000000000 --- a/frontend/src/store/feedback/actions.js +++ /dev/null @@ -1,8 +0,0 @@ -export const SHOW_FEEDBACK = 'SHOW_FEEDBACK'; -export const HIDE_FEEDBACK = 'HIDE_FEEDBACK'; - -export const showFeedback = dispatch => () => dispatch({ type: SHOW_FEEDBACK }); - -export const hideFeedback = dispatch => () => { - dispatch({ type: HIDE_FEEDBACK }); -}; diff --git a/frontend/src/store/feedback/index.js b/frontend/src/store/feedback/index.js deleted file mode 100644 index 5e4a60bbdd..0000000000 --- a/frontend/src/store/feedback/index.js +++ /dev/null @@ -1,15 +0,0 @@ -import { HIDE_FEEDBACK, SHOW_FEEDBACK } from './actions'; - -const feedback = (state = {}, action) => { - switch (action.type) { - case SHOW_FEEDBACK: - return { ...state, show: true }; - case HIDE_FEEDBACK: { - return { ...state, show: false }; - } - default: - return state; - } -}; - -export default feedback; diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index ae3be48794..13aea0e75a 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -1,40 +1,20 @@ import { combineReducers } from 'redux'; import features from './feature-toggle'; -import featureTypes from './feature-type'; -import featureMetrics from './feature-metrics'; -import featureTags from './feature-tags'; -import tagTypes from './tag-type'; -import tags from './tag'; import strategies from './strategy'; import error from './error'; import user from './user'; import applications from './application'; -import uiConfig from './ui-config'; -import context from './context'; import projects from './project'; -import addons from './addons'; import apiCalls from './api-calls'; -import invoiceAdmin from './e-admin-invoice'; -import feedback from './feedback'; const unleashStore = combineReducers({ features, - featureTypes, - featureMetrics, strategies, - tagTypes, - tags, - featureTags, error, user, applications, - uiConfig, - context, projects, - addons, apiCalls, - invoiceAdmin, - feedback, }); export default unleashStore; diff --git a/frontend/src/store/tag-type/actions.js b/frontend/src/store/tag-type/actions.js deleted file mode 100644 index cde0ef325e..0000000000 --- a/frontend/src/store/tag-type/actions.js +++ /dev/null @@ -1,77 +0,0 @@ -import api from './api'; -import { dispatchError } from '../util'; - -export const START_FETCH_TAG_TYPES = 'START_FETCH_TAG_TYPES'; -export const RECEIVE_TAG_TYPES = 'RECEIVE_TAG_TYPES'; -export const ERROR_FETCH_TAG_TYPES = 'ERROR_FETCH_TAG_TYPES'; -export const START_CREATE_TAG_TYPE = 'START_CREATE_TAG_TYPE'; -export const ADD_TAG_TYPE = 'ADD_TAG_TYPE'; -export const ERROR_CREATE_TAG_TYPE = 'ERROR_CREATE_TAG_TYPE'; -export const START_DELETE_TAG_TYPE = 'START_DELETE_TAG_TYPE'; -export const DELETE_TAG_TYPE = 'DELETE_TAG_TYPE'; -export const ERROR_DELETE_TAG_TYPE = 'ERROR_DELETE_TAG_TYPE'; -export const START_UPDATE_TAG_TYPE = 'START_UPDATE_TAG_TYPE'; -export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE'; -export const ERROR_UPDATE_TAG_TYPE = 'ERROR_UPDATE_TAG_TYPE'; - -export function receiveTagTypes(json) { - return { - type: RECEIVE_TAG_TYPES, - value: json, - receivedAt: Date.now(), - }; -} - -export function fetchTagTypes() { - return dispatch => { - dispatch({ type: START_FETCH_TAG_TYPES }); - return api - .fetchTagTypes() - .then(json => dispatch(receiveTagTypes(json))) - .catch(dispatchError(dispatch, ERROR_FETCH_TAG_TYPES)); - }; -} - -export function createTagType({ name, description, icon }) { - return dispatch => { - dispatch({ type: START_CREATE_TAG_TYPE }); - return api - .create({ name, description, icon }) - .then(() => - dispatch({ - type: ADD_TAG_TYPE, - tagType: { name, description, icon }, - }) - ) - .catch(dispatchError(dispatch, ERROR_CREATE_TAG_TYPE)); - }; -} - -export function updateTagType({ name, description, icon }) { - return dispatch => { - dispatch({ type: START_UPDATE_TAG_TYPE }); - return api - .update({ name, description, icon }) - .then(() => - dispatch({ - type: UPDATE_TAG_TYPE, - tagType: { name, description, icon }, - }) - ) - .catch(dispatchError(dispatch, ERROR_UPDATE_TAG_TYPE)); - }; -} - -export function removeTagType(name) { - return dispatch => { - dispatch({ type: START_DELETE_TAG_TYPE }); - return api - .deleteTagType(name) - .then(() => dispatch({ type: DELETE_TAG_TYPE, tagType: { name } })) - .catch(dispatchError(dispatch, ERROR_DELETE_TAG_TYPE)); - }; -} - -export function validateName(name) { - return api.validateTagType({ name }); -} diff --git a/frontend/src/store/tag-type/api.js b/frontend/src/store/tag-type/api.js deleted file mode 100644 index 53f180ac9d..0000000000 --- a/frontend/src/store/tag-type/api.js +++ /dev/null @@ -1,58 +0,0 @@ -import { formatApiPath } from '../../utils/format-path'; -import { throwIfNotSuccess, headers } from '../api-helper'; - -const URI = formatApiPath('api/admin/tag-types'); - -function fetchTagTypes() { - return fetch(URI, { credentials: 'include' }) - .then(throwIfNotSuccess) - .then(response => response.json()); -} - -function validateTagType(tagType) { - return fetch(`${URI}/validate`, { - method: 'POST', - headers, - credentials: 'include', - body: JSON.stringify(tagType), - }).then(throwIfNotSuccess); -} - -function create(tagType) { - return validateTagType(tagType) - .then(() => - fetch(URI, { - method: 'POST', - headers, - credentials: 'include', - body: JSON.stringify(tagType), - }) - ) - .then(throwIfNotSuccess); -} - -function update(tagType) { - return fetch(`${URI}/${tagType.name}`, { - method: 'PUT', - headers, - credentials: 'include', - body: JSON.stringify(tagType), - }).then(throwIfNotSuccess); -} - -function deleteTagType(tagTypeName) { - return fetch(`${URI}/${tagTypeName}`, { - method: 'DELETE', - headers, - credentials: 'include', - }).then(throwIfNotSuccess); -} - -const api = { - fetchTagTypes, - create, - update, - deleteTagType, - validateTagType, -}; -export default api; diff --git a/frontend/src/store/tag-type/index.js b/frontend/src/store/tag-type/index.js deleted file mode 100644 index 4417af4fe9..0000000000 --- a/frontend/src/store/tag-type/index.js +++ /dev/null @@ -1,33 +0,0 @@ -import { List, Map as $Map } from 'immutable'; -import { RECEIVE_TAG_TYPES, ADD_TAG_TYPE, DELETE_TAG_TYPE, UPDATE_TAG_TYPE, ERROR_FETCH_TAG_TYPES } from './actions'; - -const debug = require('debug')('unleash:tag-type-store'); - -const tagTypes = (state = new List([]), action) => { - switch (action.type) { - case ADD_TAG_TYPE: - debug('Add tagtype'); - return state.push(new $Map(action.tagType)); - case DELETE_TAG_TYPE: - debug('Delete tagtype'); - return state.filter(tagtype => tagtype.get('name') !== action.tagType.name); - case RECEIVE_TAG_TYPES: - debug('Receive tag types', action); - return new List(action.value.tagTypes.map($Map)); - case ERROR_FETCH_TAG_TYPES: - debug('Error receiving tag types', action); - return state; - case UPDATE_TAG_TYPE: - return state.map(tagtype => { - if (tagtype.get('name') === action.tagType.name) { - return new $Map(action.tagType); - } else { - return tagtype; - } - }); - default: - return state; - } -}; - -export default tagTypes; diff --git a/frontend/src/store/tag/actions.js b/frontend/src/store/tag/actions.js deleted file mode 100644 index 491b9773ff..0000000000 --- a/frontend/src/store/tag/actions.js +++ /dev/null @@ -1,50 +0,0 @@ -import api from './api'; -import { dispatchError } from '../util'; - -export const START_FETCH_TAGS = 'START_FETCH_TAGS'; -export const RECEIVE_TAGS = 'RECEIVE_TAGS'; -export const ERROR_FETCH_TAGS = 'ERROR_FETCH_TAGS'; -export const START_CREATE_TAG = 'START_CREATE_TAG'; -export const ADD_TAG = 'ADD_TAG'; -export const ERROR_CREATE_TAG = 'ERROR_CREATE_TAG'; -export const START_DELETE_TAG = 'START_DELETE_TAG'; -export const DELETE_TAG = 'DELETE_TAG'; -export const ERROR_DELETE_TAG = 'ERROR_DELETE_TAG'; - -function receiveTags(json) { - return { - type: RECEIVE_TAGS, - value: json, - receivedAt: Date.now(), - }; -} - -export function fetchTags() { - return dispatch => { - dispatch({ type: START_FETCH_TAGS }); - return api - .fetchTags() - .then(json => dispatch(receiveTags(json))) - .catch(dispatchError(dispatch, ERROR_FETCH_TAGS)); - }; -} - -export function create({ type, value }) { - return dispatch => { - dispatch({ type: START_CREATE_TAG }); - return api - .create({ type, value }) - .then(() => dispatch({ type: ADD_TAG, tag: { type, value } })) - .catch(dispatchError(dispatch, ERROR_CREATE_TAG)); - }; -} - -export function removeTag(tag) { - return dispatch => { - dispatch({ type: START_DELETE_TAG }); - return api - .deleteTag(tag) - .then(() => dispatch({ type: DELETE_TAG, tag })) - .catch(dispatchError(dispatch, ERROR_DELETE_TAG)); - }; -} diff --git a/frontend/src/store/tag/api.js b/frontend/src/store/tag/api.js deleted file mode 100644 index 17461526be..0000000000 --- a/frontend/src/store/tag/api.js +++ /dev/null @@ -1,33 +0,0 @@ -import { formatApiPath } from '../../utils/format-path'; -import { throwIfNotSuccess, headers } from '../api-helper'; - -const URI = formatApiPath('api/admin/tags'); - -function fetchTags() { - return fetch(URI, { credentials: 'include' }) - .then(throwIfNotSuccess) - .then(response => response.json()); -} - -function create(tag) { - return fetch(URI, { - method: 'POST', - headers, - credentials: 'include', - body: JSON.stringify(tag), - }).then(throwIfNotSuccess); -} - -function deleteTag(tag) { - return fetch(`${URI}/${tag.type}/${encodeURIComponent(tag.value)}`, { - method: 'DELETE', - headers, - credentials: 'include', - }).then(throwIfNotSuccess); -} - -export default { - fetchTags, - deleteTag, - create, -}; diff --git a/frontend/src/store/tag/index.js b/frontend/src/store/tag/index.js deleted file mode 100644 index 827c5c8156..0000000000 --- a/frontend/src/store/tag/index.js +++ /dev/null @@ -1,24 +0,0 @@ -import { List, Map as $Map } from 'immutable'; -import { ADD_TAG, DELETE_TAG, ERROR_FETCH_TAGS, RECEIVE_TAGS } from './actions'; -const debug = require('debug')('unleash:tag-store'); - -const tags = (state = new List([]), action) => { - switch (action.type) { - case ADD_TAG: - debug('Add tag'); - return state.push(new $Map(action.tag)); - case DELETE_TAG: - debug('Delete tag'); - return state.filter(tag => tag.get('type') !== action.tag.type && tag.get('value') !== action.tag.value); - case RECEIVE_TAGS: - debug('Receive tags', action); - return new List(action.value.tags.map($Map)); - case ERROR_FETCH_TAGS: - debug('Error receiving tag types', action); - return state; - default: - return state; - } -}; - -export default tags; diff --git a/frontend/src/store/ui-bootstrap/actions.js b/frontend/src/store/ui-bootstrap/actions.js index f61c237f1d..4f116983b7 100644 --- a/frontend/src/store/ui-bootstrap/actions.js +++ b/frontend/src/store/ui-bootstrap/actions.js @@ -1,10 +1,6 @@ import api from './api'; import { dispatchError } from '../util'; -import { receiveConfig } from '../ui-config/actions'; -import { receiveContext } from '../context/actions'; -import { receiveFeatureTypes } from '../feature-type/actions'; import { receiveProjects } from '../project/actions'; -import { receiveTagTypes } from '../tag-type/actions'; import { receiveStrategies } from '../strategy/actions'; export const RECEIVE_BOOTSTRAP = 'RECEIVE_CONFIG'; @@ -16,10 +12,6 @@ export function fetchUiBootstrap() { .fetchUIBootstrap() .then(json => { dispatch(receiveProjects(json.projects)); - dispatch(receiveConfig(json.uiConfig)); - dispatch(receiveContext(json.context)); - dispatch(receiveTagTypes(json)); - dispatch(receiveFeatureTypes(json.featureTypes)); dispatch(receiveStrategies(json.strategies)); }) .catch(dispatchError(dispatch, ERROR_RECEIVE_BOOTSTRAP)); diff --git a/frontend/src/store/ui-config/__tests__/__snapshots__/ui-config-store.test.js.snap b/frontend/src/store/ui-config/__tests__/__snapshots__/ui-config-store.test.js.snap deleted file mode 100644 index 86708e8a53..0000000000 --- a/frontend/src/store/ui-config/__tests__/__snapshots__/ui-config-store.test.js.snap +++ /dev/null @@ -1,97 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should be default state 1`] = ` -Object { - "environment": "", - "flags": Object {}, - "headerBackground": undefined, - "links": Array [ - Object { - "href": "https://docs.getunleash.io/docs?source=oss", - "icon": Object { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": Object { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "title": "User documentation", - "value": "Documentation", - }, - Object { - "href": "https://github.com/Unleash", - "icon": "c_github", - "title": "Source code on GitHub", - "value": "GitHub", - }, - ], - "name": "Unleash", - "slogan": "The enterprise ready feature toggle service.", - "version": "3.x", -} -`; - -exports[`should be merged state all 1`] = ` -Object { - "environment": "dev", - "flags": Object {}, - "headerBackground": "red", - "links": Array [ - Object { - "href": "https://docs.getunleash.io/docs?source=oss", - "icon": Object { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": Object { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "title": "User documentation", - "value": "Documentation", - }, - Object { - "href": "https://github.com/Unleash", - "icon": "c_github", - "title": "Source code on GitHub", - "value": "GitHub", - }, - ], - "name": "Unleash", - "slogan": "hello", - "version": "3.x", -} -`; - -exports[`should only update headerBackground 1`] = ` -Object { - "environment": "", - "flags": Object {}, - "headerBackground": "black", - "links": Array [ - Object { - "href": "https://docs.getunleash.io/docs?source=oss", - "icon": Object { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": Object { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "title": "User documentation", - "value": "Documentation", - }, - Object { - "href": "https://github.com/Unleash", - "icon": "c_github", - "title": "Source code on GitHub", - "value": "GitHub", - }, - ], - "name": "Unleash", - "slogan": "The enterprise ready feature toggle service.", - "version": "3.x", -} -`; diff --git a/frontend/src/store/ui-config/__tests__/ui-config-store.test.js b/frontend/src/store/ui-config/__tests__/ui-config-store.test.js deleted file mode 100644 index d9bcd0d1ed..0000000000 --- a/frontend/src/store/ui-config/__tests__/ui-config-store.test.js +++ /dev/null @@ -1,31 +0,0 @@ -import reducer from '../index'; -import { receiveConfig } from '../actions'; - -beforeEach(() => { - localStorage.clear(); -}); - -test('should be default state', () => { - const state = reducer(undefined, {}); - expect(state.toJS()).toMatchSnapshot(); -}); - -test('should be merged state all', () => { - const uiConfig = { - headerBackground: 'red', - slogan: 'hello', - environment: 'dev', - }; - - const state = reducer(undefined, receiveConfig(uiConfig)); - expect(state.toJS()).toMatchSnapshot(); -}); - -test('should only update headerBackground', () => { - const uiConfig = { - headerBackground: 'black', - }; - - const state = reducer(undefined, receiveConfig(uiConfig)); - expect(state.toJS()).toMatchSnapshot(); -}); diff --git a/frontend/src/store/ui-config/actions.js b/frontend/src/store/ui-config/actions.js deleted file mode 100644 index 2e88eb1018..0000000000 --- a/frontend/src/store/ui-config/actions.js +++ /dev/null @@ -1,18 +0,0 @@ -import api from './api'; -import { dispatchError } from '../util'; - -export const RECEIVE_CONFIG = 'RECEIVE_CONFIG'; -export const ERROR_RECEIVE_CONFIG = 'ERROR_RECEIVE_CONFIG'; - -export const receiveConfig = json => ({ - type: RECEIVE_CONFIG, - value: json, -}); - -export function fetchUIConfig() { - return dispatch => - api - .fetchConfig() - .then(json => dispatch(receiveConfig(json))) - .catch(dispatchError(dispatch, ERROR_RECEIVE_CONFIG)); -} diff --git a/frontend/src/store/ui-config/api.js b/frontend/src/store/ui-config/api.js deleted file mode 100644 index c340ea414c..0000000000 --- a/frontend/src/store/ui-config/api.js +++ /dev/null @@ -1,14 +0,0 @@ -import { formatApiPath } from '../../utils/format-path'; -import { throwIfNotSuccess } from '../api-helper'; - -const URI = formatApiPath('api/admin/ui-config'); - -function fetchConfig() { - return fetch(URI, { credentials: 'include' }) - .then(throwIfNotSuccess) - .then(response => response.json()); -} - -export default { - fetchConfig, -}; diff --git a/frontend/src/store/ui-config/index.js b/frontend/src/store/ui-config/index.js deleted file mode 100644 index 221e52ebf8..0000000000 --- a/frontend/src/store/ui-config/index.js +++ /dev/null @@ -1,61 +0,0 @@ -import { Map as $Map } from 'immutable'; -import { LibraryBooks } from '@material-ui/icons'; -import { RECEIVE_CONFIG } from './actions'; - -import { getBasePath } from '../../utils/format-path'; - -const localStorage = window.localStorage || { - setItem: () => {}, - getItem: () => {}, -}; - -const basePath = getBasePath(); -const UI_CONFIG = `${basePath}:ui_config`; - -const DEFAULT = new $Map({ - headerBackground: undefined, - name: 'Unleash', - version: '3.x', - environment: '', - slogan: 'The enterprise ready feature toggle service.', - flags: {}, - links: [ - { - value: 'Documentation', - icon: LibraryBooks, - href: 'https://docs.getunleash.io/docs?source=oss', - title: 'User documentation', - }, - { - value: 'GitHub', - icon: 'c_github', - href: 'https://github.com/Unleash', - title: 'Source code on GitHub', - }, - ], -}); - -function getInitState() { - try { - const state = JSON.parse(localStorage.getItem(UI_CONFIG)); - return state ? DEFAULT.merge(state) : DEFAULT; - } catch (e) { - return DEFAULT; - } -} - -function updateConfig(state, config) { - localStorage.setItem(UI_CONFIG, JSON.stringify(config)); - return state.merge(config); -} - -const strategies = (state = getInitState(), action) => { - switch (action.type) { - case RECEIVE_CONFIG: - return updateConfig(state, action.value); - default: - return state; - } -}; - -export default strategies; diff --git a/frontend/src/themes/main-theme.js b/frontend/src/themes/main-theme.ts similarity index 87% rename from frontend/src/themes/main-theme.js rename to frontend/src/themes/main-theme.ts index 70c1443632..81eb4bfa34 100644 --- a/frontend/src/themes/main-theme.js +++ b/frontend/src/themes/main-theme.ts @@ -1,6 +1,16 @@ -import { createTheme } from '@material-ui/core/styles'; +import { createTheme, Theme } from '@material-ui/core/styles'; -const theme = createTheme({ +type MainTheme = typeof mainTheme; + +declare module '@material-ui/core/styles/createTheme' { + interface Theme extends MainTheme {} +} + +declare module '@material-ui/core/styles/makeStyles' { + interface Theme extends MainTheme {} +} + +const mainTheme = { typography: { fontFamily: ['Sen', 'Roboto, sans-serif'], fontWeightBold: '700', @@ -117,6 +127,6 @@ const theme = createTheme({ semi: '700', bold: '700', }, -}); +}; -export default theme; +export default createTheme(mainTheme as unknown as Theme); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 83a892a0de..56c8d82aff 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2077,6 +2077,18 @@ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/lodash.clonedeep@^4.5.6": + version "4.5.6" + resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.6.tgz#3b6c40a0affe0799a2ce823b440a6cf33571d32b" + integrity sha512-cE1jYr2dEg1wBImvXlNtp0xDoS79rfEdGozQVgliDZj1uERH4k+rmEMTudP9b4VQ8O6nRb5gPqft0QzEQGMQgA== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.178" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" + integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw== + "@types/minimatch@*": version "3.0.4" resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz"