1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-15 17:50:48 +02:00

Migrate to create-react-app and react-scripts (#263)

* Setup create-react-app and typescript

Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
Christopher Kolstad 2021-04-07 09:04:48 +02:00 committed by GitHub
parent 30e3f468eb
commit 22795e251f
80 changed files with 5397 additions and 3855 deletions

View File

@ -1,13 +0,0 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime"
],
"env": {
"test": {
"plugins": ["@babel/plugin-transform-modules-commonjs"]
}
}
}

View File

@ -1,3 +0,0 @@
node_modules
bundle.js
dist

View File

@ -1,25 +0,0 @@
{
"extends": [
"finn",
"finn/node",
"finn-prettier"
],
"rules": {
"no-shadow": 0,
"prettier/prettier": [
2,
{
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 120
}
]
},
"overrides": [
{
"files": ["**/__tests__/*"],
"env": { "jest": true }
}
]
}

View File

@ -25,4 +25,4 @@ jobs:
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: yarn - run: yarn
- run: yarn run test:ci - run: yarn run test

1
frontend/.gitignore vendored
View File

@ -41,6 +41,7 @@ typings/
# Built # Built
dist dist
build
# IDE # IDE
.idea/ .idea/

View File

@ -1,13 +0,0 @@
<!doctype html>
<html>
<head>
<title>Unleash [development]</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet">
</head>
<body>
<div id='app'></div>
<script src="/static/bundle.js"></script>
</body>
</html>

View File

@ -1,7 +1,7 @@
'use strict';
const path = require('path'); const path = require('path');
module.exports = { module.exports = {
publicFolder: path.join(__dirname, 'dist'), publicFolder: path.join(__dirname, 'build'),
}; };

View File

@ -1,6 +0,0 @@
'use strict';
// We have set timezone to make sure tests are correct
module.exports = () => {
process.env.TZ = 'UTC';
};

View File

@ -1,7 +1,7 @@
{ {
"name": "unleash-frontend", "name": "unleash-frontend",
"description": "unleash your features", "description": "unleash your features",
"version": "4.0.0-alpha.1", "version": "3.15.1",
"keywords": [ "keywords": [
"unleash", "unleash",
"feature toggle", "feature toggle",
@ -10,7 +10,7 @@
], ],
"files": [ "files": [
"index.js", "index.js",
"dist/" "build/"
], ],
"repository": { "repository": {
"type": "git", "type": "git",
@ -20,106 +20,93 @@
"url": "https://github.com/Unleash/unleash-frontend" "url": "https://github.com/Unleash/unleash-frontend"
}, },
"engines": { "engines": {
"node": ">=10" "node": ">=12"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {
"build": "npm run build:assets && npm run build:html && npm run build:img && npm run build:ico", "build": "react-scripts build",
"build:assets": "NODE_ENV=production webpack -p --display-optimization-bailout", "start": "react-scripts start",
"build:html": "cp public/*.html dist/.", "start:heroku": "UNLEASH_API=https://unleash.herokuapp.com yarn run start",
"build:ico": "cp public/*.ico dist/.", "test": "react-scripts test",
"build:img": "cp public/*.png dist/public/. && cp public/*.svg dist/public/.", "prepublish": "yarn run build"
"start": "NODE_ENV=development webpack-dev-server --progress --colors",
"start:heroku": "UNLEASH_API=https://unleash.herokuapp.com npm run start",
"lint": "eslint . --ext js,jsx",
"lint:fix": "eslint . --ext js,jsx --fix",
"test": "jest",
"test:ci": "npm run lint && npm run build && npm run test",
"prepublish": "npm run build"
}, },
"main": "./index.js", "dependencies": {
"devDependencies": {
"@material-ui/lab": "4.0.0-alpha.57",
"@babel/core": "^7.9.0",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.8.3",
"@babel/plugin-transform-modules-commonjs": "^7.9.0",
"@babel/plugin-transform-runtime": "^7.9.0",
"@babel/preset-env": "^7.9.5",
"@babel/preset-react": "^7.9.4",
"@material-ui/core": "^4.11.3", "@material-ui/core": "^4.11.3",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@material-ui/styles": "^4.11.3", "@material-ui/lab": "^4.0.0-alpha.57",
"array-move": "^2.2.1", "@testing-library/jest-dom": "^5.11.4",
"babel-eslint": "^10.1.0", "@testing-library/react": "^11.1.0",
"babel-jest": "^25.3.0", "@testing-library/user-event": "^12.1.10",
"babel-loader": "^8.1.0", "@types/enzyme": "^3.10.8",
"classnames": "^2.2.6", "@types/enzyme-adapter-react-16": "^1.0.6",
"clean-webpack-plugin": "^3.0.0", "@types/jest": "^26.0.15",
"css-loader": "^2.1.1", "@types/node": "^12.0.0",
"date-fns": "^2.17.0", "@types/react": "^17.0.0",
"debounce": "^1.2.0", "@types/react-dom": "^17.0.0",
"debug": "^4.1.1", "@types/react-router-dom": "^5.1.7",
"enzyme": "^3.9.0", "array-move": "^3.0.1",
"enzyme-adapter-react-16": "^1.11.0", "classnames": "^2.3.1",
"enzyme-to-json": "^3.3.5", "css-loader": "^5.2.0",
"eslint": "^6.5.1", "date-fns": "^2.19.0",
"eslint-config-finn": "^3.0.1", "debounce": "^1.2.1",
"eslint-config-finn-prettier": "^3.0.2", "enzyme": "^3.11.0",
"eslint-config-finn-react": "^2.0.2", "enzyme-adapter-react-16": "^1.15.6",
"eslint-plugin-react": "^7.11.1",
"fetch-mock": "^9.11.0", "fetch-mock": "^9.11.0",
"identity-obj-proxy": "^3.0.0", "immutable": "^4.0.0-rc.12",
"immutable": "^3.8.1",
"jest": "^26.6.3",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lodash.flow": "^3.5.0", "lodash.flow": "^3.5.0",
"mini-css-extract-plugin": "^0.9.0",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"node-sass": "^4.5.3", "react": "^17.0.2",
"normalize.css": "^8.0.0", "react-dnd": "^14.0.2",
"optimize-css-assets-webpack-plugin": "^5.0.0", "react-dnd-html5-backend": "^14.0.0",
"prettier": "^1.18.2", "react-dom": "^17.0.2",
"prop-types": "^15.6.2", "react-redux": "^7.2.3",
"react": "^16.14.0", "react-router-dom": "^5.2.0",
"react-dnd": "^11.1.3", "react-scripts": "4.0.3",
"react-dnd-html5-backend": "^11.1.3", "react-timeago": "^5.2.0",
"react-dom": "^16.14.0",
"react-redux": "^7.2.0",
"react-router-dom": "^5.1.2",
"react-test-renderer": "^16.14.0",
"react-timeago": "^4.4.0",
"redux": "^4.0.5", "redux": "^4.0.5",
"redux-devtools": "^3.7.0", "redux-devtools-extension": "^2.13.9",
"redux-mock-store": "^1.5.4", "redux-mock-store": "^1.5.4",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"sass-loader": "^7.0.1", "sass": "^1.32.8",
"style-loader": "^1.0.0", "typescript": "^4.2.3",
"toolbox-loader": "0.0.3", "web-vitals": "^1.0.1"
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.17.1",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.11.2",
"whatwg-fetch": "^3.4.1"
}, },
"jest": { "jest": {
"moduleNameMapper": { "moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/src/__mocks__/fileMock.js", "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/src/__mocks__/fileMock.js",
"\\.(css|scss)$": "identity-obj-proxy" "\\.(css|scss)$": "identity-obj-proxy"
}, },
"setupFilesAfterEnv": [
"<rootDir>/src/setupTests.js"
],
"setupFiles": [
"<rootDir>/jest-setup.js"
],
"snapshotSerializers": [ "snapshotSerializers": [
"enzyme-to-json/serializer" "enzyme-to-json/serializer"
],
"testPathIgnorePatterns": [
"/src/store/addons/__tests__/data.js"
] ]
}, },
"dependencies": {} "browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"rules": {
"no-restricted-globals": "off",
"no-useless-computed-key": "off",
"import/no-anonymous-default-export": "off"
}
},
"proxy": "http://localhost:4242",
"devDependencies": {
"enzyme-to-json": "^3.6.1"
}
} }

View File

@ -2,17 +2,16 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="unleash"> <meta name="description" content="unleash">
<title>Unleash - Enterprise ready feature toggles</title> <title>Unleash - Enterprise ready feature toggles</title>
<link rel="stylesheet" href="public/bundle.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet">
</head> </head>
<body> <body>
<div id='app'></div> <div id='app'></div>
<script src="public/bundle.js"></script>
</body> </body>
</html> </html>

View File

@ -1,37 +0,0 @@
{
"parser": "babel-eslint",
"extends": [
"finn",
"finn-react",
"finn/es-modules",
"finn-prettier",
"finn-prettier/react"
],
"env": {
"browser": true,
"commonjs": true,
"es6": true
},
"globals": {
"process": false
},
"parserOptions": {
"ecmaVersion": 7,
"ecmaFeatures": {
"experimentalObjectRestSpread": true
}
},
"rules": {
"no-shadow": 0,
"react/sort-comp": 0,
"prettier/prettier": [
2,
{
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 120
}
]
}
}

View File

@ -20,7 +20,7 @@ const Reporting = ({ fetchFeatureToggles, projects }) => {
useEffect(() => { useEffect(() => {
fetchFeatureToggles(); fetchFeatureToggles();
setSelectedProject(projects[0].id); setSelectedProject(projects[0].id);
}, []); }, [fetchFeatureToggles, projects]);
useEffect(() => { useEffect(() => {
setProjectOptions(formatProjectOptions(projects)); setProjectOptions(formatProjectOptions(projects));
@ -43,7 +43,7 @@ const Reporting = ({ fetchFeatureToggles, projects }) => {
options={projectOptions} options={projectOptions}
value={selectedProject} value={selectedProject}
onChange={onChange} onChange={onChange}
inputProps={{ ['data-test']: REPORTING_SELECT_ID }} inputProps={{ ['data-testid']: REPORTING_SELECT_ID }}
/> />
</div> </div>
); );

View File

@ -3,7 +3,8 @@ import { Provider } from 'react-redux';
import { HashRouter } from 'react-router-dom'; import { HashRouter } from 'react-router-dom';
import { createStore } from 'redux'; import { createStore } from 'redux';
import { mount } from 'enzyme/build';
import { render, screen, fireEvent } from '@testing-library/react';
import Reporting from '../Reporting'; import Reporting from '../Reporting';
import { REPORTING_SELECT_ID } from '../../../testIds'; import { REPORTING_SELECT_ID } from '../../../testIds';
@ -16,8 +17,8 @@ const mockStore = {
}; };
const mockReducer = state => state; const mockReducer = state => state;
test('changing projects renders only toggles from that project', () => { test('changing projects renders only toggles from that project', async () => {
const wrapper = mount( render(
<HashRouter> <HashRouter>
<Provider store={createStore(mockReducer, mockStore)}> <Provider store={createStore(mockReducer, mockStore)}>
<Reporting projects={testProjects} features={testFeatures} fetchFeatureToggles={() => {}} /> <Reporting projects={testProjects} features={testFeatures} fetchFeatureToggles={() => {}} />
@ -25,14 +26,9 @@ test('changing projects renders only toggles from that project', () => {
</HashRouter> </HashRouter>
); );
const select = wrapper.find(`input[data-test="${REPORTING_SELECT_ID}"][value="default"]`).first(); const table = await screen.findByRole("table");
let list = wrapper.find('tr');
/* Length of projects belonging to project (3) + header row (1) */ /* Length of projects belonging to project (3) + header row (1) */
expect(list.length).toBe(4); expect(table.rows).toHaveLength(4);
fireEvent.change(await screen.findByTestId(REPORTING_SELECT_ID), { target: { value: 'myProject'}});
select.simulate('change', { target: { value: 'myProject' } }); expect(table.rows).toHaveLength(3);
list = wrapper.find('tr');
expect(list.length).toBe(3);
}); });

View File

@ -15,11 +15,11 @@ const style = {
const getIcon = name => { const getIcon = name => {
switch (name) { switch (name) {
case 'slack': case 'slack':
return <img style={style} src="public/slack.svg" />; return <img style={style} alt="Slack Logo" src="slack.svg" />;
case 'jira-comment': case 'jira-comment':
return <img style={style} src="public/jira.svg" />; return <img style={style} alt="JIRA Logo" src="jira.svg" />;
case 'webhook': case 'webhook':
return <img style={style} src="public/webhooks.svg" />; return <img style={style} alt="Generic Webhook logo" src="webhooks.svg" />;
default: default:
return ( return (
<Avatar> <Avatar>
@ -34,7 +34,8 @@ const AddonList = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, h
if (addons.length === 0) { if (addons.length === 0) {
fetchAddons(); fetchAddons();
} }
}, []); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [addons.length]);
return ( return (
<> <>

View File

@ -22,17 +22,17 @@ const AddonFormComponent = ({ editMode, provider, addon, fetch, cancel, submit }
if (!provider) { if (!provider) {
fetch(); fetch();
} }
}, []); // empty array => fetch only first time }, [fetch, provider]); // empty array => fetch only first time
useEffect(() => { useEffect(() => {
setConfig({ ...addon }); setConfig({ ...addon });
}, [addon.id]); }, [addon]);
useEffect(() => { useEffect(() => {
if (provider && !config.provider) { if (provider && !config.provider) {
setConfig({ ...addon, provider: provider.name }); setConfig({ ...addon, provider: provider.name });
} }
}, [provider]); }, [provider, addon, config.provider]);
const setFieldValue = field => evt => { const setFieldValue = field => evt => {
evt.preventDefault(); evt.preventDefault();
@ -104,7 +104,7 @@ const AddonFormComponent = ({ editMode, provider, addon, fetch, cancel, submit }
<PageContent headerContent={`Configure ${name} addon`}> <PageContent headerContent={`Configure ${name} addon`}>
<section className={styles.formSection}> <section className={styles.formSection}>
{description}&nbsp; {description}&nbsp;
<a href={documentationUrl} target="_blank"> <a href={documentationUrl} target="_blank" rel="noreferrer">
Read more Read more
</a> </a>
<p className={commonStyles.error}>{errors.general}</p> <p className={commonStyles.error}>{errors.general}</p>

View File

@ -1,15 +1,12 @@
import { DndProvider, createDndContext } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import React, { useRef } from 'react'; import React from 'react';
const RNDContext = createDndContext(HTML5Backend);
function useDNDProviderElement(props) { function useDNDProviderElement(props) {
const manager = useRef(RNDContext);
if (!props.children) return null; if (!props.children) return null;
return <DndProvider manager={manager.current.dragDropManager}>{props.children}</DndProvider>; return <DndProvider backend={HTML5Backend}>{props.children}</DndProvider>;
} }
export default function DragAndDrop(props) { export default function DragAndDrop(props) {

View File

@ -108,7 +108,7 @@ export function getIcon(type) {
} }
export const IconLink = ({ url, icon }) => ( export const IconLink = ({ url, icon }) => (
<a href={url} target="_blank" rel="noopener" className="mdl-color-text--grey-600"> <a href={url} target="_blank" rel="noreferrer" className="mdl-color-text--grey-600">
<Icon>{icon}</Icon> <Icon>{icon}</Icon>
</a> </a>
); );

View File

@ -188,6 +188,7 @@ class AddContextComponent extends Component {
<a <a
href="https://unleash.github.io/docs/activation_strategy#flexiblerollout" href="https://unleash.github.io/docs/activation_strategy#flexiblerollout"
target="_blank" target="_blank"
rel="noreferrer"
> >
Read more Read more
</a> </a>

View File

@ -5,6 +5,8 @@ import { MenuItem } from '@material-ui/core';
import { MenuItemWithIcon } from '../../../common'; import { MenuItemWithIcon } from '../../../common';
import DropdownMenu from '../../../common/dropdown-menu'; import DropdownMenu from '../../../common/dropdown-menu';
import ProjectSelect from '../../../common/ProjectSelect'; import ProjectSelect from '../../../common/ProjectSelect';
import { useStyles } from './styles';
import classnames from 'classnames';
const sortingOptions = [ const sortingOptions = [
{ type: 'name', displayName: 'Name' }, { type: 'name', displayName: 'Name' },
@ -17,8 +19,6 @@ const sortingOptions = [
{ type: 'metrics', displayName: 'Metrics' }, { type: 'metrics', displayName: 'Metrics' },
]; ];
import { useStyles } from './styles';
import classnames from 'classnames';
const FeatureToggleListActions = ({ settings, setSort, toggleMetrics, updateSetting, loading }) => { const FeatureToggleListActions = ({ settings, setSort, toggleMetrics, updateSetting, loading }) => {
const styles = useStyles(); const styles = useStyles();

View File

@ -52,6 +52,7 @@ const FeatureView = ({
useEffect(() => { useEffect(() => {
scrollToTop(); scrollToTop();
fetchTags(featureToggleName); fetchTags(featureToggleName);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
useLayoutEffect(() => { useLayoutEffect(() => {
@ -62,6 +63,7 @@ const FeatureView = ({
fetchArchive(); fetchArchive();
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [features]); }, [features]);
const getTabComponent = key => { const getTabComponent = key => {
@ -91,6 +93,8 @@ const FeatureView = ({
); );
case 'log': case 'log':
return <HistoryComponent toggleName={featureToggleName} />; return <HistoryComponent toggleName={featureToggleName} />;
default:
return null
} }
}; };
const getTabData = () => [ const getTabData = () => [

View File

@ -103,7 +103,6 @@ class CopyFeatureComponent extends Component {
label="Feature toggle name" label="Feature toggle name"
name="name" name="name"
value={newToggleName} value={newToggleName}
error={nameError}
onBlur={this.onValidateName} onBlur={this.onValidateName}
onChange={this.setValue} onChange={this.setValue}
error={nameError !== undefined} error={nameError !== undefined}

View File

@ -20,11 +20,11 @@ function FeatureTagComponent({ tags, tagTypes, featureToggleName, untagFeature }
if (tagType && tagType.icon) { if (tagType && tagType.icon) {
switch (tagType.name) { switch (tagType.name) {
case 'slack': case 'slack':
return <img style={style} alt="slack" src="public/slack.svg" />; return <img style={style} alt="slack" src="slack.svg" />;
case 'jira': case 'jira':
return <img style={style} alt="jira" src="public/jira.svg" />; return <img style={style} alt="jira" src="jira.svg" />;
case 'webhook': case 'webhook':
return <img style={style} alt="webhook" src="public/webhooks.svg" />; return <img style={style} alt="webhook" src="webhooks.svg" />;
default: default:
return <Icon>label</Icon>; return <Icon>label</Icon>;
} }

View File

@ -42,7 +42,7 @@ export default function GeneralStrategyInput({ parameters, strategyDefinition, u
maxLabel="on" maxLabel="on"
/> />
{description && ( {description && (
<p className={styles.helpText} className={styles.helperText}> <p className={styles.helpText}>
{description} {description}
</p> </p>
)} )}

View File

@ -67,6 +67,8 @@ const StrategyCardContentCustom = ({ strategy, strategyDefinition }) => {
show={<StrategyCardField title={paramDefinition.name} value={param} />} show={<StrategyCardField title={paramDefinition.name} value={param} />}
/> />
); );
default:
return null
} }
}; };

View File

@ -34,7 +34,7 @@ const StrategiesList = props => {
if (!editStrategyIndex) { if (!editStrategyIndex) {
updateEditableStrategies(cloneDeep(props.configuredStrategies)); updateEditableStrategies(cloneDeep(props.configuredStrategies));
} }
}, [props.configuredStrategies]); }, [props.configuredStrategies, editStrategyIndex]);
const updateStrategy = index => strategy => { const updateStrategy = index => strategy => {
const newStrategy = { ...strategy }; const newStrategy = { ...strategy };

View File

@ -499,6 +499,7 @@ exports[`renders correctly with with variants 1`] = `
<a <a
href="https://unleash.github.io/docs/toggle_variants" href="https://unleash.github.io/docs/toggle_variants"
rel="noreferrer"
target="_blank" target="_blank"
> >
Read more Read more

View File

@ -1,9 +1,9 @@
import React, { useState, useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { TextField, FormControl, FormControlLabel, Grid, Icon, Switch } from '@material-ui/core'; import { FormControl, FormControlLabel, Grid, Icon, Switch, TextField } from '@material-ui/core';
import Dialog from '../../common/Dialogue'; import Dialog from '../../common/Dialogue';
import MySelect from '../../common/select'; import MySelect from '../../common/select';
import { trim, modalStyles } from '../../common/util'; import { modalStyles, trim } from '../../common/util';
import { weightTypes } from './enums'; import { weightTypes } from './enums';
import OverrideConfig from './e-override-config'; import OverrideConfig from './e-override-config';
@ -46,6 +46,7 @@ function AddVariant({ showDialog, closeDialog, save, validateName, editVariant,
useEffect(() => { useEffect(() => {
clear(); clear();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editVariant]); }, [editVariant]);
const setVariantValue = e => { const setVariantValue = e => {

View File

@ -111,7 +111,7 @@ class UpdateVariantComponent extends Component {
> >
By overriding the stickiness you can control which parameter you want to be used in order to ensure By overriding the stickiness you can control which parameter you want to be used in order to ensure
consistent traffic allocation across variants.{' '} consistent traffic allocation across variants.{' '}
<a href="https://unleash.github.io/docs/toggle_variants" target="_blank"> <a href="https://unleash.github.io/docs/toggle_variants" target="_blank" rel="noreferrer">
Read more Read more
</a> </a>
</small> </small>

View File

@ -33,7 +33,7 @@ export const Footer = () => (
<ListItem key="github_link" className={styles.listItem}> <ListItem key="github_link" className={styles.listItem}>
<ListItemText <ListItemText
primary={ primary={
<a href="https://github.com/Unleash/unleash/" target="_blank"> <a href="https://github.com/Unleash/unleash/" target="_blank" rel="noreferrer">
GitHub GitHub
</a> </a>
} }

View File

@ -20,6 +20,7 @@ const Header = ({ uiConfig, init }) => {
useEffect(() => { useEffect(() => {
init(uiConfig.flags); init(uiConfig.flags);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const toggleDrawer = () => setOpenDrawer(prev => !prev); const toggleDrawer = () => setOpenDrawer(prev => !prev);

View File

@ -1,10 +1,10 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { loadInitialData } from '../../../store/loader'; import { loadInitialData } from '../../../store/loader';
const mapStateToProps = state => ({ uiConfig: state.uiConfig.toJS() });
import Header from './Header'; import Header from './Header';
const mapStateToProps = state => ({ uiConfig: state.uiConfig.toJS() });
export default connect(mapStateToProps, { export default connect(mapStateToProps, {
init: loadInitialData, init: loadInitialData,
})(Header); })(Header);

View File

@ -43,7 +43,7 @@ function renderLink(link, toggleDrawer) {
key={link.href} key={link.href}
target="_blank" target="_blank"
className={[styles.navigationLink].join(' ')} className={[styles.navigationLink].join(' ')}
title={link.title} title={link.title} rel="noreferrer"
> >
{getIcon(link.icon)} {link.value} {getIcon(link.icon)} {link.value}
</a> </a>
@ -56,7 +56,7 @@ export const DrawerMenu = ({ links = [], title = 'Unleash', flags = {}, open = f
<div className={styles.drawerContainer}> <div className={styles.drawerContainer}>
<div className={styles.drawerTitleContainer}> <div className={styles.drawerTitleContainer}>
<span className={[styles.drawerTitle].join(' ')}> <span className={[styles.drawerTitle].join(' ')}>
<img src="public/logo.png" width="32" height="32" className={styles.drawerTitleLogo} /> <img alt="Unleash Logo" src="logo.png" width="32" height="32" className={styles.drawerTitleLogo} />
<span className={styles.drawerTitleText}>{title}</span> <span className={styles.drawerTitleText}>{title}</span>
</span> </span>
</div> </div>

View File

@ -15,7 +15,7 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history, hasPermi
const styles = useStyles(); const styles = useStyles();
useEffect(() => { useEffect(() => {
fetchProjects(); fetchProjects();
}, []); }, [fetchProjects]);
const addProjectButton = () => ( const addProjectButton = () => (
<ConditionallyRender <ConditionallyRender

View File

@ -1,24 +1,24 @@
import React, { useState, useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
Avatar,
Button,
Card, Card,
CardHeader, CardHeader,
Avatar,
List,
ListItem,
ListItemSecondaryAction,
ListItemText,
ListItemAvatar,
Select,
MenuItem,
Icon,
IconButton,
Dialog, Dialog,
DialogActions, DialogActions,
DialogTitle,
DialogContentText,
DialogContent, DialogContent,
Button, DialogContentText,
DialogTitle,
Icon,
IconButton,
List,
ListItem,
ListItemAvatar,
ListItemSecondaryAction,
ListItemText,
MenuItem,
Select,
} from '@material-ui/core'; } from '@material-ui/core';
import AddUserComponent from './access-add-user'; import AddUserComponent from './access-add-user';
@ -38,6 +38,7 @@ function AccessComponent({ projectId, project }) {
useEffect(() => { useEffect(() => {
fetchAccess(); fetchAccess();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]); }, [projectId]);
if (!project) { if (!project) {

View File

@ -27,6 +27,7 @@ const StrategiesList = ({
useEffect(() => { useEffect(() => {
fetchStrategies(); fetchStrategies();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const headerButton = () => ( const headerButton = () => (

View File

@ -29,6 +29,7 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType, hasPermission })
useEffect(() => { useEffect(() => {
fetchTagTypes(); fetchTagTypes();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
let header = ( let header = (

View File

@ -18,6 +18,7 @@ const TagList = ({ tags, fetchTags, removeTag, hasPermission }) => {
useEffect(() => { useEffect(() => {
fetchTags(); fetchTags();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const remove = (tag, evt) => { const remove = (tag, evt) => {

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CardActions, Button, TextField, Typography } from '@material-ui/core'; import { CardActions, Button, TextField, Typography, IconButton } from '@material-ui/core';
import ConditionallyRender from '../../common/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { useCommonStyles } from '../../../common.styles'; import { useCommonStyles } from '../../../common.styles';
@ -121,9 +121,9 @@ const PasswordAuth = ({ authDetails, passwordLogin, loadInitialData }) => {
condition={showFields} condition={showFields}
show={renderLoginForm()} show={renderLoginForm()}
elseShow={ elseShow={
<a href="" onClick={onShowOptions}> <IconButton> onClick={onShowOptions}>
Show more options Show more options
</a> </IconButton>
} }
/> />
</div> </div>

View File

@ -29,7 +29,7 @@ const SimpleAuth = ({ insecureLogin, loadInitialData, history, authDetails }) =>
<p> <p>
This instance of Unleash is not set up with a secure authentication provider. You can read more This instance of Unleash is not set up with a secure authentication provider. You can read more
about{' '} about{' '}
<a href="https://github.com/Unleash/unleash/blob/master/docs/securing-unleash.md" target="_blank"> <a href="https://github.com/Unleash/unleash/blob/master/docs/securing-unleash.md" target="_blank" rel="noreferrer">
securing Unleash on GitHub securing Unleash on GitHub
</a> </a>
</p> </p>

View File

@ -1,25 +1,24 @@
import React from 'react'; import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Card, CardContent, CardHeader } from '@material-ui/core'; import { Card, CardContent, CardHeader } from '@material-ui/core';
import { styles as commonStyles } from '../common'; import { styles as commonStyles } from '../common';
export default class FeatureListComponent extends React.Component { const LogoutComponent = ({logoutUser}) => {
static propTypes = { useEffect(() => {
logoutUser: PropTypes.func.isRequired, logoutUser();
}; });
componentDidMount() { return (<Card shadow={0} className={commonStyles.fullwidth}>
this.props.logoutUser(); <CardHeader>Logged out</CardHeader>
} <CardContent>
You have now been successfully logged out of Unleash. Thank you for using Unleash.{' '}
render() { </CardContent>
return ( </Card>
<Card shadow={0} className={commonStyles.fullwidth}> );
<CardHeader>Logged out</CardHeader>
<CardContent>
You have now been successfully logged out of Unleash. Thank you for using Unleash.{' '}
</CardContent>
</Card>
);
}
} }
LogoutComponent.propTypes = {
logoutUser: PropTypes.func.isRequired
}
export default LogoutComponent;

View File

@ -49,8 +49,8 @@ export default class ShowUserComponent extends React.Component {
const email = this.props.profile ? this.props.profile.email : ''; const email = this.props.profile ? this.props.profile.email : '';
const locale = this.getLocale(); const locale = this.getLocale();
let foundLocale = this.possibleLocales.find(l => l.value === locale); let foundLocale = this.possibleLocales.find(l => l.value === locale);
const imageUrl = email ? this.props.profile.imageUrl : 'public/unknown-user.png'; const imageUrl = email ? this.props.profile.imageUrl : 'unknown-user.png';
const imageLocale = foundLocale ? `public/${foundLocale.image}.png` : `public/unknown-locale.png`; const imageLocale = foundLocale ? `${foundLocale.image}.png` : `unknown-locale.png`;
return ( return (
<div className={styles.showUserSettings}> <div className={styles.showUserSettings}>
<DropdownMenu <DropdownMenu
@ -60,7 +60,7 @@ export default class ShowUserComponent extends React.Component {
this.possibleLocales.map(i => ( this.possibleLocales.map(i => (
<MenuItem key={i.value} onClick={() => this.setLocale(i)}> <MenuItem key={i.value} onClick={() => this.setLocale(i)}>
<div className={styles.showLocale}> <div className={styles.showLocale}>
<img src={`public/${i.image}.png`} title={i.value} alt={i.value} /> <img src={`${i.image}.png`} title={i.value} alt={i.value} />
<Typography variant="p">{i.value}</Typography> <Typography variant="p">{i.value}</Typography>
</div> </div>
</MenuItem> </MenuItem>

View File

@ -18,10 +18,11 @@ import App from './component/app';
import ScrollToTop from './component/scroll-to-top'; import ScrollToTop from './component/scroll-to-top';
import { writeWarning } from './security-logger'; import { writeWarning } from './security-logger';
let composeEnhancers; let composeEnhancers;
if (process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { if (process.env.NODE_ENV !== 'production' && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) {
composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
} else { } else {
composeEnhancers = compose; composeEnhancers = compose;
writeWarning(); writeWarning();

View File

@ -12,7 +12,7 @@ function ApiHowTo() {
}} }}
> >
Read the{' '} Read the{' '}
<a href="https://www.unleash-hosted.com/docs" target="_blank"> <a href="https://www.unleash-hosted.com/docs" target="_blank" rel="noreferrer">
Getting started guide Getting started guide
</a>{' '} </a>{' '}
to learn how to connect to the Unleash API form your application or programmatically. <br /> <br /> to learn how to connect to the Unleash API form your application or programmatically. <br /> <br />

View File

@ -20,6 +20,7 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermis
useEffect(() => { useEffect(() => {
fetchApiKeys(); fetchApiKeys();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
return ( return (

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Icon } from '@material-ui/core'; import { Icon, IconButton } from '@material-ui/core';
function Secret({ value }) { function Secret({ value }) {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
@ -17,9 +17,9 @@ function Secret({ value }) {
<span>***************************</span> <span>***************************</span>
)} )}
<a href="" onClick={toggle} title="Show token"> <IconButton aria-label="Show token" onClick={toggle} title="Show token">
<Icon style={{ marginLeft: '5px', fontSize: '1.2em' }}>visibility</Icon> <Icon style={{ marginLeft: '5px', fontSize: '1.2em' }}>visibility</Icon>
</a> </IconButton>
</div> </div>
); );
} }

View File

@ -15,6 +15,7 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission
useEffect(() => { useEffect(() => {
getGoogleConfig(); getGoogleConfig();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -59,7 +60,7 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission
<Grid item xs={12}> <Grid item xs={12}>
<Typography variant="subtitle1"> <Typography variant="subtitle1">
Please read the{' '} Please read the{' '}
<a href="https://www.unleash-hosted.com/docs/enterprise-authentication/google" target="_blank"> <a href="https://www.unleash-hosted.com/docs/enterprise-authentication/google" target="_blank" rel="noreferrer">
documentation documentation
</a>{' '} </a>{' '}
to learn how to integrate with Google OAuth 2.0. <br /> to learn how to integrate with Google OAuth 2.0. <br />

View File

@ -15,12 +15,14 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) {
useEffect(() => { useEffect(() => {
getSamlConfig(); getSamlConfig();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
useEffect(() => { useEffect(() => {
if (config.entityId) { if (config.entityId) {
setData(config); setData(config);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]); }, [config]);
if (!hasPermission('ADMIN')) { if (!hasPermission('ADMIN')) {
@ -59,7 +61,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) {
<Grid item md={12}> <Grid item md={12}>
<Typography variant="subtitle1"> <Typography variant="subtitle1">
Please read the{' '} Please read the{' '}
<a href="https://www.unleash-hosted.com/docs/enterprise-authentication" target="_blank"> <a href="https://www.unleash-hosted.com/docs/enterprise-authentication" target="_blank" rel="noreferrer">
documentation documentation
</a>{' '} </a>{' '}
to learn how to integrate with specific SAML 2.0 providers (Okta, Keycloak, etc). <br /> to learn how to integrate with specific SAML 2.0 providers (Okta, Keycloak, etc). <br />

View File

@ -1,7 +1,7 @@
/* eslint-disable no-alert */ /* eslint-disable no-alert */
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button, Icon, Table, TableBody, TableCell, TableHead, TableRow } from '@material-ui/core'; import { Button, Icon, IconButton, Table, TableBody, TableCell, TableHead, TableRow } from '@material-ui/core';
import { formatFullDateTimeWithLocale } from '../../../../component/common/util'; import { formatFullDateTimeWithLocale } from '../../../../component/common/util';
import AddUser from '../add-user-component'; import AddUser from '../add-user-component';
import ChangePassword from '../change-password-component'; import ChangePassword from '../change-password-component';
@ -65,6 +65,7 @@ function UsersList({
useEffect(() => { useEffect(() => {
fetchUsers(); fetchUsers();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
return ( return (
@ -92,22 +93,22 @@ function UsersList({
condition={hasPermission('ADMIN')} condition={hasPermission('ADMIN')}
show={ show={
<TableCell> <TableCell>
<a href="" title="Edit" onClick={openUpdateDialog(item)}> <IconButton aria-label="Edit" title="Edit" onClick={openUpdateDialog(item)}>
<Icon>edit</Icon> <Icon>edit</Icon>
</a> </IconButton>
<a href="" title="Change password" onClick={openPwDialog(item)}> <IconButton aria-label="Change password" title="Change password" onClick={openPwDialog(item)}>
<Icon>lock</Icon> <Icon>lock</Icon>
</a> </IconButton>
<a href="" title="Remove user" onClick={openDelDialog(item)}> <IconButton aria-label="Remove user" title="Remove user" onClick={openDelDialog(item)}>
<Icon>delete</Icon> <Icon>delete</Icon>
</a> </IconButton>
</TableCell> </TableCell>
} }
elseShow={ elseShow={
<TableCell> <TableCell>
<a href="" title="Change password" onClick={openPwDialog(item)}> <IconButton aria-label="Change password" title="Change password" onClick={openPwDialog(item)}>
<Icon>lock</Icon> <Icon>lock</Icon>
</a> </IconButton>
</TableCell> </TableCell>
} }
/> />

View File

@ -13,12 +13,13 @@ import {
import { showPermissions, modalStyles } from './util'; import { showPermissions, modalStyles } from './util';
function AddUser({ user, showDialog, closeDialog, updateUser }) { function AddUser({ user, showDialog, closeDialog, updateUser }) {
const [data, setData] = useState(user);
const [error, setError] = useState({});
if (!user) { if (!user) {
return null; return null;
} }
const [data, setData] = useState(user);
const [error, setError] = useState({});
const updateField = e => { const updateField = e => {
setData({ setData({

1
frontend/src/react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@ -1,5 +1,6 @@
import '@testing-library/jest-dom'
import { configure } from 'enzyme'; import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16'; import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
process.env.TZ = 'UTC'; process.env.TZ = 'UTC';
configure({ adapter: new Adapter() });

View File

@ -1,6 +1,6 @@
import reducer from '../index'; import reducer from '../index';
import { RECEIVE_ADDON_CONFIG, ADD_ADDON_CONFIG, REMOVE_ADDON_CONFIG, UPDATE_ADDON_CONFIG } from '../actions'; import { RECEIVE_ADDON_CONFIG, ADD_ADDON_CONFIG, REMOVE_ADDON_CONFIG, UPDATE_ADDON_CONFIG } from '../actions';
import { addonSimple, addonsWithConfig, addonConfig } from './data'; import { addonSimple, addonsWithConfig, addonConfig } from '../__testdata__/data';
import { USER_LOGOUT } from '../../user/actions'; import { USER_LOGOUT } from '../../user/actions';
test('should be default state', () => { test('should be default state', () => {

View File

@ -1,5 +1,5 @@
import api from './api'; import api from './api';
import { dispatchAndThrow } from '../util'; import { dispatchError } from '../util';
export const RECEIVE_ADDON_CONFIG = 'RECEIVE_ADDON_CONFIG'; export const RECEIVE_ADDON_CONFIG = 'RECEIVE_ADDON_CONFIG';
export const ERROR_RECEIVE_ADDON_CONFIG = 'ERROR_RECEIVE_ADDON_CONFIG'; export const ERROR_RECEIVE_ADDON_CONFIG = 'ERROR_RECEIVE_ADDON_CONFIG';
@ -22,7 +22,7 @@ export function fetchAddons() {
api api
.fetchAll() .fetchAll()
.then(success(dispatch, RECEIVE_ADDON_CONFIG)) .then(success(dispatch, RECEIVE_ADDON_CONFIG))
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_ADDON_CONFIG)); .catch(dispatchError(dispatch, ERROR_RECEIVE_ADDON_CONFIG));
} }
export function removeAddon(addon) { export function removeAddon(addon) {
@ -30,7 +30,7 @@ export function removeAddon(addon) {
api api
.remove(addon) .remove(addon)
.then(() => dispatch(removeAddonconfig(addon))) .then(() => dispatch(removeAddonconfig(addon)))
.catch(dispatchAndThrow(dispatch, ERROR_REMOVING_ADDON_CONFIG)); .catch(dispatchError(dispatch, ERROR_REMOVING_ADDON_CONFIG));
} }
export function createAddon(addon) { export function createAddon(addon) {
@ -39,7 +39,7 @@ export function createAddon(addon) {
.create(addon) .create(addon)
.then(res => res.json()) .then(res => res.json())
.then(value => dispatch(addAddonConfig(value))) .then(value => dispatch(addAddonConfig(value)))
.catch(dispatchAndThrow(dispatch, ERROR_ADD_ADDON_CONFIG)); .catch(dispatchError(dispatch, ERROR_ADD_ADDON_CONFIG));
} }
export function updateAddon(addon) { export function updateAddon(addon) {
@ -47,5 +47,5 @@ export function updateAddon(addon) {
api api
.update(addon) .update(addon)
.then(() => dispatch(updateAdddonConfig(addon))) .then(() => dispatch(updateAdddonConfig(addon)))
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_ADDON_CONFIG)); .catch(dispatchError(dispatch, ERROR_UPDATE_ADDON_CONFIG));
} }

View File

@ -34,9 +34,10 @@ function remove(addonConfig) {
}).then(throwIfNotSuccess); }).then(throwIfNotSuccess);
} }
export default { const api = {
fetchAll, fetchAll,
create, create,
update, update,
remove, remove,
}; };
export default api;

View File

@ -1,5 +1,5 @@
import api from './api'; import api from './api';
import { dispatchAndThrow } from '../util'; import { dispatchError } from '../util';
import { MUTE_ERROR } from '../error/actions'; import { MUTE_ERROR } from '../error/actions';
export const RECEIVE_ALL_APPLICATIONS = 'RECEIVE_ALL_APPLICATIONS'; export const RECEIVE_ALL_APPLICATIONS = 'RECEIVE_ALL_APPLICATIONS';
@ -26,7 +26,7 @@ export function fetchAll() {
api api
.fetchAll() .fetchAll()
.then(json => dispatch(recieveAllApplications(json))) .then(json => dispatch(recieveAllApplications(json)))
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_ALL_APPLICATIONS)); .catch(dispatchError(dispatch, ERROR_RECEIVE_ALL_APPLICATIONS));
} }
export function storeApplicationMetaData(appName, key, value) { export function storeApplicationMetaData(appName, key, value) {
@ -38,7 +38,7 @@ export function storeApplicationMetaData(appName, key, value) {
setTimeout(() => dispatch({ type: MUTE_ERROR, error: info }), 1000); setTimeout(() => dispatch({ type: MUTE_ERROR, error: info }), 1000);
dispatch({ type: UPDATE_APPLICATION_FIELD, appName, key, value, info }); dispatch({ type: UPDATE_APPLICATION_FIELD, appName, key, value, info });
}) })
.catch(dispatchAndThrow(dispatch, ERROR_UPDATING_APPLICATION_DATA)); .catch(dispatchError(dispatch, ERROR_UPDATING_APPLICATION_DATA));
} }
export function fetchApplication(appName) { export function fetchApplication(appName) {
@ -46,7 +46,7 @@ export function fetchApplication(appName) {
api api
.fetchApplication(appName) .fetchApplication(appName)
.then(json => dispatch(recieveApplication(json))) .then(json => dispatch(recieveApplication(json)))
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_ALL_APPLICATIONS)); .catch(dispatchError(dispatch, ERROR_RECEIVE_ALL_APPLICATIONS));
} }
export function deleteApplication(appName) { export function deleteApplication(appName) {
@ -54,5 +54,5 @@ export function deleteApplication(appName) {
api api
.deleteApplication(appName) .deleteApplication(appName)
.then(() => dispatch({ type: DELETE_APPLICATION, appName })) .then(() => dispatch({ type: DELETE_APPLICATION, appName }))
.catch(dispatchAndThrow(dispatch, ERROR_DELETE_APPLICATION)); .catch(dispatchError(dispatch, ERROR_DELETE_APPLICATION));
} }

View File

@ -1,5 +1,5 @@
import api from './api'; import api from './api';
import { dispatchAndThrow } from '../util'; import { dispatchError } from '../util';
export const REVIVE_TOGGLE = 'REVIVE_TOGGLE'; export const REVIVE_TOGGLE = 'REVIVE_TOGGLE';
export const RECEIVE_ARCHIVE = 'RECEIVE_ARCHIVE'; export const RECEIVE_ARCHIVE = 'RECEIVE_ARCHIVE';
@ -20,7 +20,7 @@ export function revive(featureToggle) {
api api
.revive(featureToggle) .revive(featureToggle)
.then(() => dispatch(reviveToggle(featureToggle))) .then(() => dispatch(reviveToggle(featureToggle)))
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_ARCHIVE)); .catch(dispatchError(dispatch, ERROR_RECEIVE_ARCHIVE));
} }
export function fetchArchive() { export function fetchArchive() {
@ -28,5 +28,5 @@ export function fetchArchive() {
api api
.fetchAll() .fetchAll()
.then(json => dispatch(receiveArchive(json))) .then(json => dispatch(receiveArchive(json)))
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_ARCHIVE)); .catch(dispatchError(dispatch, ERROR_RECEIVE_ARCHIVE));
} }

View File

@ -1,5 +1,5 @@
import api from './api'; import api from './api';
import { dispatchAndThrow } from '../util'; import { dispatchError } from '../util';
export const RECEIVE_CONTEXT = 'RECEIVE_CONTEXT'; export const RECEIVE_CONTEXT = 'RECEIVE_CONTEXT';
export const ERROR_RECEIVE_CONTEXT = 'ERROR_RECEIVE_CONTEXT'; export const ERROR_RECEIVE_CONTEXT = 'ERROR_RECEIVE_CONTEXT';
@ -23,7 +23,7 @@ export function fetchContext() {
json.sort((a, b) => a.sortOrder - b.sortOrder); json.sort((a, b) => a.sortOrder - b.sortOrder);
dispatch(receiveContext(json)); dispatch(receiveContext(json));
}) })
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_CONTEXT)); .catch(dispatchError(dispatch, ERROR_RECEIVE_CONTEXT));
} }
export function removeContextField(context) { export function removeContextField(context) {
@ -31,7 +31,7 @@ export function removeContextField(context) {
api api
.remove(context) .remove(context)
.then(() => dispatch(createRemoveContext(context))) .then(() => dispatch(createRemoveContext(context)))
.catch(dispatchAndThrow(dispatch, ERROR_REMOVING_CONTEXT)); .catch(dispatchError(dispatch, ERROR_REMOVING_CONTEXT));
} }
export function createContextField(context) { export function createContextField(context) {
@ -39,7 +39,7 @@ export function createContextField(context) {
api api
.create(context) .create(context)
.then(() => dispatch(addContextField(context))) .then(() => dispatch(addContextField(context)))
.catch(dispatchAndThrow(dispatch, ERROR_ADD_CONTEXT_FIELD)); .catch(dispatchError(dispatch, ERROR_ADD_CONTEXT_FIELD));
} }
export function updateContextField(context) { export function updateContextField(context) {
@ -47,7 +47,7 @@ export function updateContextField(context) {
api api
.update(context) .update(context)
.then(() => dispatch(upContextField(context))) .then(() => dispatch(upContextField(context)))
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_CONTEXT_FIELD)); .catch(dispatchError(dispatch, ERROR_UPDATE_CONTEXT_FIELD));
} }
export function validateName(name) { export function validateName(name) {

View File

@ -1,5 +1,5 @@
import api from './api'; import api from './api';
import { dispatchAndThrow } from '../util'; import { dispatchError } from '../util';
export const RECIEVE_GOOGLE_CONFIG = 'RECIEVE_GOOGLE_CONFIG'; export const RECIEVE_GOOGLE_CONFIG = 'RECIEVE_GOOGLE_CONFIG';
export const RECIEVE_GOOGLE_CONFIG_ERROR = 'RECIEVE_GOOGLE_CONFIG_ERROR'; export const RECIEVE_GOOGLE_CONFIG_ERROR = 'RECIEVE_GOOGLE_CONFIG_ERROR';
export const UPDATE_GOOGLE_AUTH = 'UPDATE_GOOGLE_AUTH'; export const UPDATE_GOOGLE_AUTH = 'UPDATE_GOOGLE_AUTH';
@ -22,7 +22,7 @@ export function getGoogleConfig() {
config, config,
}) })
) )
.catch(dispatchAndThrow(dispatch, RECIEVE_GOOGLE_CONFIG_ERROR)); .catch(dispatchError(dispatch, RECIEVE_GOOGLE_CONFIG_ERROR));
} }
export function updateGoogleConfig(data) { export function updateGoogleConfig(data) {
@ -30,7 +30,7 @@ export function updateGoogleConfig(data) {
api api
.updateGoogleConfig(data) .updateGoogleConfig(data)
.then(config => dispatch({ type: UPDATE_GOOGLE_AUTH, config })) .then(config => dispatch({ type: UPDATE_GOOGLE_AUTH, config }))
.catch(dispatchAndThrow(dispatch, UPDATE_GOOGLE_AUTH_ERROR)); .catch(dispatchError(dispatch, UPDATE_GOOGLE_AUTH_ERROR));
} }
export function getSamlConfig() { export function getSamlConfig() {
@ -44,7 +44,7 @@ export function getSamlConfig() {
config, config,
}) })
) )
.catch(dispatchAndThrow(dispatch, RECIEVE_SAML_CONFIG_ERROR)); .catch(dispatchError(dispatch, RECIEVE_SAML_CONFIG_ERROR));
} }
export function updateSamlConfig(data) { export function updateSamlConfig(data) {
@ -52,5 +52,5 @@ export function updateSamlConfig(data) {
api api
.updateSamlConfig(data) .updateSamlConfig(data)
.then(config => dispatch({ type: UPDATE_SAML_AUTH, config })) .then(config => dispatch({ type: UPDATE_SAML_AUTH, config }))
.catch(dispatchAndThrow(dispatch, UPDATE_SAML_AUTH_ERROR)); .catch(dispatchError(dispatch, UPDATE_SAML_AUTH_ERROR));
} }

View File

@ -1,5 +1,5 @@
import api from './api'; import api from './api';
import { dispatchAndThrow } from '../util'; import { dispatchError } from '../util';
export const RECIEVE_KEYS = 'RECIEVE_KEYS'; export const RECIEVE_KEYS = 'RECIEVE_KEYS';
export const ERROR_FETCH_KEYS = 'ERROR_FETCH_KEYS'; export const ERROR_FETCH_KEYS = 'ERROR_FETCH_KEYS';
export const REMOVE_KEY = 'REMOVE_KEY'; export const REMOVE_KEY = 'REMOVE_KEY';
@ -20,7 +20,7 @@ export function fetchApiKeys() {
tokens: value.tokens, tokens: value.tokens,
}) })
) )
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_KEYS)); .catch(dispatchError(dispatch, ERROR_FETCH_KEYS));
} }
export function removeKey(secret) { export function removeKey(secret) {
@ -28,7 +28,7 @@ export function removeKey(secret) {
api api
.remove(secret) .remove(secret)
.then(() => dispatch({ type: REMOVE_KEY, secret })) .then(() => dispatch({ type: REMOVE_KEY, secret }))
.catch(dispatchAndThrow(dispatch, REMOVE_KEY)); .catch(dispatchError(dispatch, REMOVE_KEY));
} }
export function addKey(data) { export function addKey(data) {
@ -36,5 +36,5 @@ export function addKey(data) {
api api
.create(data) .create(data)
.then(newToken => dispatch({ type: ADD_KEY, token: newToken })) .then(newToken => dispatch({ type: ADD_KEY, token: newToken }))
.catch(dispatchAndThrow(dispatch, ADD_KEY_ERROR)); .catch(dispatchError(dispatch, ADD_KEY_ERROR));
} }

View File

@ -1,5 +1,5 @@
import api from './api'; import api from './api';
import { dispatchAndThrow } from '../util'; import { dispatchError } from '../util';
export const START_FETCH_USERS = 'START_FETCH_USERS'; export const START_FETCH_USERS = 'START_FETCH_USERS';
export const RECIEVE_USERS = 'RECIEVE_USERS'; export const RECIEVE_USERS = 'RECIEVE_USERS';
export const ERROR_FETCH_USERS = 'ERROR_FETCH_USERS'; export const ERROR_FETCH_USERS = 'ERROR_FETCH_USERS';
@ -27,7 +27,7 @@ export function fetchUsers() {
return api return api
.fetchAll() .fetchAll()
.then(json => dispatch(gotUsers(json))) .then(json => dispatch(gotUsers(json)))
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_USERS)); .catch(dispatchError(dispatch, ERROR_FETCH_USERS));
}; };
} }
@ -36,7 +36,7 @@ export function removeUser(user) {
api api
.remove(user) .remove(user)
.then(() => dispatch({ type: REMOVE_USER, user })) .then(() => dispatch({ type: REMOVE_USER, user }))
.catch(dispatchAndThrow(dispatch, REMOVE_USER_ERROR)); .catch(dispatchError(dispatch, REMOVE_USER_ERROR));
} }
export function addUser(user) { export function addUser(user) {
@ -44,7 +44,7 @@ export function addUser(user) {
api api
.create(user) .create(user)
.then(newUser => dispatch({ type: ADD_USER, user: newUser })) .then(newUser => dispatch({ type: ADD_USER, user: newUser }))
.catch(dispatchAndThrow(dispatch, ADD_USER_ERROR)); .catch(dispatchError(dispatch, ADD_USER_ERROR));
} }
export function updateUser(user) { export function updateUser(user) {
@ -52,13 +52,13 @@ export function updateUser(user) {
api api
.update(user) .update(user)
.then(newUser => dispatch({ type: UPDATE_USER, user: newUser })) .then(newUser => dispatch({ type: UPDATE_USER, user: newUser }))
.catch(dispatchAndThrow(dispatch, UPDATE_USER_ERROR)); .catch(dispatchError(dispatch, UPDATE_USER_ERROR));
} }
export function changePassword(user, newPassword) { export function changePassword(user, newPassword) {
return dispatch => api.changePassword(user, newPassword).catch(dispatchAndThrow(dispatch, CHANGE_PASSWORD_ERROR)); return dispatch => api.changePassword(user, newPassword).catch(dispatchError(dispatch, CHANGE_PASSWORD_ERROR));
} }
export function validatePassword(password) { export function validatePassword(password) {
return dispatch => api.validatePassword(password).catch(dispatchAndThrow(dispatch, VALIDATE_PASSWORD_ERROR)); return dispatch => api.validatePassword(password).catch(dispatchError(dispatch, VALIDATE_PASSWORD_ERROR));
} }

View File

@ -1,5 +1,5 @@
import api from './api'; import api from './api';
import { dispatchAndThrow } from '../util'; import { dispatchError } from '../util';
export const TAG_FEATURE_TOGGLE = 'TAG_FEATURE_TOGGLE'; export const TAG_FEATURE_TOGGLE = 'TAG_FEATURE_TOGGLE';
export const UNTAG_FEATURE_TOGGLE = 'UNTAG_FEATURE_TOGGLE'; export const UNTAG_FEATURE_TOGGLE = 'UNTAG_FEATURE_TOGGLE';
@ -25,7 +25,7 @@ export function tagFeature(featureToggle, tag) {
return api return api
.tagFeature(featureToggle, tag) .tagFeature(featureToggle, tag)
.then(json => dispatch({ type: TAG_FEATURE_TOGGLE, featureToggle, tag: json })) .then(json => dispatch({ type: TAG_FEATURE_TOGGLE, featureToggle, tag: json }))
.catch(dispatchAndThrow(dispatch, ERROR_TAG_FEATURE_TOGGLE)); .catch(dispatchError(dispatch, ERROR_TAG_FEATURE_TOGGLE));
}; };
} }
@ -35,7 +35,7 @@ export function untagFeature(featureToggle, tag) {
return api return api
.untagFeature(featureToggle, tag) .untagFeature(featureToggle, tag)
.then(() => dispatch({ type: UNTAG_FEATURE_TOGGLE, featureToggle, tag })) .then(() => dispatch({ type: UNTAG_FEATURE_TOGGLE, featureToggle, tag }))
.catch(dispatchAndThrow(dispatch, ERROR_UNTAG_FEATURE_TOGGLE)); .catch(dispatchError(dispatch, ERROR_UNTAG_FEATURE_TOGGLE));
}; };
} }
@ -45,6 +45,6 @@ export function fetchTags(featureToggle) {
return api return api
.fetchFeatureToggleTags(featureToggle) .fetchFeatureToggleTags(featureToggle)
.then(json => dispatch(receiveFeatureToggleTags(json))) .then(json => dispatch(receiveFeatureToggleTags(json)))
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_FEATURE_TOGGLE_TAGS)); .catch(dispatchError(dispatch, ERROR_FETCH_FEATURE_TOGGLE_TAGS));
}; };
} }

View File

@ -1,7 +1,7 @@
import api from './api'; import api from './api';
const debug = require('debug')('unleash:feature-actions'); import { dispatchError } from '../util';
import { dispatchAndThrow } from '../util';
import { MUTE_ERROR } from '../error/actions'; import { MUTE_ERROR } from '../error/actions';
const debug = require('debug')('unleash:feature-actions');
export const ADD_FEATURE_TOGGLE = 'ADD_FEATURE_TOGGLE'; export const ADD_FEATURE_TOGGLE = 'ADD_FEATURE_TOGGLE';
export const COPY_FEATURE_TOGGLE = 'COPY_FEATURE_TOGGLE'; export const COPY_FEATURE_TOGGLE = 'COPY_FEATURE_TOGGLE';
@ -77,7 +77,7 @@ export function fetchFeatureToggles() {
}) })
.catch(() => { .catch(() => {
dispatch({ type: FETCH_FEATURE_TOGGLE_ERROR }); dispatch({ type: FETCH_FEATURE_TOGGLE_ERROR });
dispatchAndThrow(dispatch, ERROR_FETCH_FEATURE_TOGGLES); dispatchError(dispatch, ERROR_FETCH_FEATURE_TOGGLES);
}); });
}; };
} }
@ -91,7 +91,7 @@ export function fetchFeatureToggle(name) {
return api return api
.fetchFeatureToggle(name) .fetchFeatureToggle(name)
.then(json => dispatch(receiveFeatureToggle(json))) .then(json => dispatch(receiveFeatureToggle(json)))
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_FEATURE_TOGGLE)); .catch(dispatchError(dispatch, ERROR_FETCH_FEATURE_TOGGLE));
}; };
} }
@ -108,7 +108,7 @@ export function createFeatureToggles(featureToggle) {
featureToggle: createdFeature, featureToggle: createdFeature,
}); });
}) })
.catch(dispatchAndThrow(dispatch, ERROR_CREATING_FEATURE_TOGGLE)); .catch(dispatchError(dispatch, ERROR_CREATING_FEATURE_TOGGLE));
}; };
} }
@ -119,7 +119,7 @@ export function requestToggleFeatureToggle(enable, name) {
return api return api
.toggle(enable, name) .toggle(enable, name)
.then(() => dispatch({ type: TOGGLE_FEATURE_TOGGLE, name })) .then(() => dispatch({ type: TOGGLE_FEATURE_TOGGLE, name }))
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_FEATURE_TOGGLE)); .catch(dispatchError(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
}; };
} }
@ -134,7 +134,7 @@ export function requestSetStaleFeatureToggle(stale, name) {
setTimeout(() => dispatch({ type: MUTE_ERROR, error: info }), 1000); setTimeout(() => dispatch({ type: MUTE_ERROR, error: info }), 1000);
dispatch({ type: UPDATE_FEATURE_TOGGLE, featureToggle, info }); dispatch({ type: UPDATE_FEATURE_TOGGLE, featureToggle, info });
}) })
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_FEATURE_TOGGLE)); .catch(dispatchError(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
}; };
} }
@ -149,7 +149,7 @@ export function requestUpdateFeatureToggle(featureToggle) {
setTimeout(() => dispatch({ type: MUTE_ERROR, error: info }), 1000); setTimeout(() => dispatch({ type: MUTE_ERROR, error: info }), 1000);
dispatch({ type: UPDATE_FEATURE_TOGGLE, featureToggle, info }); dispatch({ type: UPDATE_FEATURE_TOGGLE, featureToggle, info });
}) })
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_FEATURE_TOGGLE)); .catch(dispatchError(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
}; };
} }
@ -169,7 +169,7 @@ export function requestUpdateFeatureToggleStrategies(featureToggle, newStrategie
info, info,
}); });
}) })
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_FEATURE_TOGGLE)); .catch(dispatchError(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
}; };
} }
@ -189,7 +189,7 @@ export function requestUpdateFeatureToggleVariants(featureToggle, newVariants) {
info, info,
}); });
}) })
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_FEATURE_TOGGLE)); .catch(dispatchError(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
}; };
} }
@ -200,7 +200,7 @@ export function removeFeatureToggle(featureToggleName) {
return api return api
.remove(featureToggleName) .remove(featureToggleName)
.then(() => dispatch({ type: REMOVE_FEATURE_TOGGLE, featureToggleName })) .then(() => dispatch({ type: REMOVE_FEATURE_TOGGLE, featureToggleName }))
.catch(dispatchAndThrow(dispatch, ERROR_REMOVE_FEATURE_TOGGLE)); .catch(dispatchError(dispatch, ERROR_REMOVE_FEATURE_TOGGLE));
}; };
} }

View File

@ -1,6 +1,4 @@
import { List, Map as $Map } from 'immutable'; import { List, Map as $Map } from 'immutable';
const debug = require('debug')('unleash:feature-store');
import { import {
ADD_FEATURE_TOGGLE, ADD_FEATURE_TOGGLE,
RECEIVE_FEATURE_TOGGLES, RECEIVE_FEATURE_TOGGLES,
@ -13,6 +11,8 @@ import {
import { USER_LOGOUT, USER_LOGIN } from '../user/actions'; import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
const debug = require('debug')('unleash:feature-store');
const features = (state = new List([]), action) => { const features = (state = new List([]), action) => {
switch (action.type) { switch (action.type) {
case ADD_FEATURE_TOGGLE: case ADD_FEATURE_TOGGLE:

View File

@ -1,5 +1,5 @@
import api from './api'; import api from './api';
import { dispatchAndThrow } from '../util'; import { dispatchError } from '../util';
export const RECEIVE_FEATURE_TYPES = 'RECEIVE_FEATURE_TYPES'; export const RECEIVE_FEATURE_TYPES = 'RECEIVE_FEATURE_TYPES';
export const ERROR_RECEIVE_FEATURE_TYPES = 'ERROR_RECEIVE_FEATURE_TYPES'; export const ERROR_RECEIVE_FEATURE_TYPES = 'ERROR_RECEIVE_FEATURE_TYPES';
@ -11,5 +11,5 @@ export function fetchFeatureTypes() {
api api
.fetchAll() .fetchAll()
.then(json => dispatch(receiveFeatureTypes(json))) .then(json => dispatch(receiveFeatureTypes(json)))
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_FEATURE_TYPES)); .catch(dispatchError(dispatch, ERROR_RECEIVE_FEATURE_TYPES));
} }

View File

@ -1,5 +1,5 @@
import api from './api'; import api from './api';
import { dispatchAndThrow } from '../util'; import { dispatchError } from '../util';
export const RECEIVE_HISTORY = 'RECEIVE_HISTORY'; export const RECEIVE_HISTORY = 'RECEIVE_HISTORY';
export const ERROR_RECEIVE_HISTORY = 'ERROR_RECEIVE_HISTORY'; export const ERROR_RECEIVE_HISTORY = 'ERROR_RECEIVE_HISTORY';
@ -21,7 +21,7 @@ export function fetchHistory() {
api api
.fetchAll() .fetchAll()
.then(json => dispatch(receiveHistory(json))) .then(json => dispatch(receiveHistory(json)))
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_HISTORY)); .catch(dispatchError(dispatch, ERROR_RECEIVE_HISTORY));
} }
export function fetchHistoryForToggle(toggleName) { export function fetchHistoryForToggle(toggleName) {
@ -29,5 +29,5 @@ export function fetchHistoryForToggle(toggleName) {
api api
.fetchHistoryForToggle(toggleName) .fetchHistoryForToggle(toggleName)
.then(json => dispatch(receiveHistoryforToggle(json))) .then(json => dispatch(receiveHistoryforToggle(json)))
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_HISTORY)); .catch(dispatchError(dispatch, ERROR_RECEIVE_HISTORY));
} }

View File

@ -1,5 +1,5 @@
import api from './api'; import api from './api';
import { dispatchAndThrow } from '../util'; import { dispatchError } from '../util';
export const RECEIVE_PROJECT = 'RECEIVE_PROJECT'; export const RECEIVE_PROJECT = 'RECEIVE_PROJECT';
export const ERROR_RECEIVE_PROJECT = 'ERROR_RECEIVE_PROJECT'; export const ERROR_RECEIVE_PROJECT = 'ERROR_RECEIVE_PROJECT';
@ -22,7 +22,7 @@ export function fetchProjects() {
.then(json => { .then(json => {
dispatch(receiveProjects(json.projects)); dispatch(receiveProjects(json.projects));
}) })
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_PROJECT)); .catch(dispatchError(dispatch, ERROR_RECEIVE_PROJECT));
} }
export function removeProject(project) { export function removeProject(project) {
@ -30,7 +30,7 @@ export function removeProject(project) {
api api
.remove(project) .remove(project)
.then(() => dispatch(delProject(project))) .then(() => dispatch(delProject(project)))
.catch(dispatchAndThrow(dispatch, ERROR_REMOVING_PROJECT)); .catch(dispatchError(dispatch, ERROR_REMOVING_PROJECT));
} }
export function createProject(project) { export function createProject(project) {
@ -38,7 +38,7 @@ export function createProject(project) {
api api
.create(project) .create(project)
.then(() => dispatch(addProject(project))) .then(() => dispatch(addProject(project)))
.catch(dispatchAndThrow(dispatch, ERROR_ADD_PROJECT)); .catch(dispatchError(dispatch, ERROR_ADD_PROJECT));
} }
export function updateProject(project) { export function updateProject(project) {
@ -46,7 +46,7 @@ export function updateProject(project) {
api api
.update(project) .update(project)
.then(() => dispatch(upProject(project))) .then(() => dispatch(upProject(project)))
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_PROJECT)); .catch(dispatchError(dispatch, ERROR_UPDATE_PROJECT));
} }
export function validateId(id) { export function validateId(id) {

View File

@ -1,6 +1,6 @@
import api from './api'; import api from './api';
import applicationApi from '../application/api'; import applicationApi from '../application/api';
import { dispatchAndThrow } from '../util'; import { dispatchError } from '../util';
export const ADD_STRATEGY = 'ADD_STRATEGY'; export const ADD_STRATEGY = 'ADD_STRATEGY';
export const UPDATE_STRATEGY = 'UPDATE_STRATEGY'; export const UPDATE_STRATEGY = 'UPDATE_STRATEGY';
@ -44,7 +44,7 @@ export function fetchStrategies() {
return api return api
.fetchAll() .fetchAll()
.then(json => dispatch(receiveStrategies(json))) .then(json => dispatch(receiveStrategies(json)))
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_STRATEGIES)); .catch(dispatchError(dispatch, ERROR_RECEIVE_STRATEGIES));
}; };
} }
@ -55,7 +55,7 @@ export function createStrategy(strategy) {
return api return api
.create(strategy) .create(strategy)
.then(() => dispatch(addStrategy(strategy))) .then(() => dispatch(addStrategy(strategy)))
.catch(dispatchAndThrow(dispatch, ERROR_CREATING_STRATEGY)); .catch(dispatchError(dispatch, ERROR_CREATING_STRATEGY));
}; };
} }
@ -66,7 +66,7 @@ export function updateStrategy(strategy) {
return api return api
.update(strategy) .update(strategy)
.then(() => dispatch(updatedStrategy(strategy))) .then(() => dispatch(updatedStrategy(strategy)))
.catch(dispatchAndThrow(dispatch, ERROR_UPDATING_STRATEGY)); .catch(dispatchError(dispatch, ERROR_UPDATING_STRATEGY));
}; };
} }
@ -75,7 +75,7 @@ export function removeStrategy(strategy) {
api api
.remove(strategy) .remove(strategy)
.then(() => dispatch(createRemoveStrategy(strategy))) .then(() => dispatch(createRemoveStrategy(strategy)))
.catch(dispatchAndThrow(dispatch, ERROR_REMOVING_STRATEGY)); .catch(dispatchError(dispatch, ERROR_REMOVING_STRATEGY));
} }
export function getApplicationsWithStrategy(strategyName) { export function getApplicationsWithStrategy(strategyName) {
@ -87,7 +87,7 @@ export function deprecateStrategy(strategy) {
dispatch(startDeprecate()); dispatch(startDeprecate());
api.deprecate(strategy) api.deprecate(strategy)
.then(() => dispatch(deprecateStrategyEvent(strategy))) .then(() => dispatch(deprecateStrategyEvent(strategy)))
.catch(dispatchAndThrow(dispatch, ERROR_DEPRECATING_STRATEGY)); .catch(dispatchError(dispatch, ERROR_DEPRECATING_STRATEGY));
}; };
} }
@ -96,6 +96,6 @@ export function reactivateStrategy(strategy) {
dispatch(startReactivate()); dispatch(startReactivate());
api.reactivate(strategy) api.reactivate(strategy)
.then(() => dispatch(reactivateStrategyEvent(strategy))) .then(() => dispatch(reactivateStrategyEvent(strategy)))
.catch(dispatchAndThrow(dispatch, ERROR_REACTIVATING_STRATEGY)); .catch(dispatchError(dispatch, ERROR_REACTIVATING_STRATEGY));
}; };
} }

View File

@ -1,5 +1,5 @@
import api from './api'; import api from './api';
import { dispatchAndThrow } from '../util'; import { dispatchError } from '../util';
export const START_FETCH_TAG_TYPES = 'START_FETCH_TAG_TYPES'; export const START_FETCH_TAG_TYPES = 'START_FETCH_TAG_TYPES';
export const RECEIVE_TAG_TYPES = 'RECEIVE_TAG_TYPES'; export const RECEIVE_TAG_TYPES = 'RECEIVE_TAG_TYPES';
@ -28,7 +28,7 @@ export function fetchTagTypes() {
return api return api
.fetchTagTypes() .fetchTagTypes()
.then(json => dispatch(receiveTagTypes(json))) .then(json => dispatch(receiveTagTypes(json)))
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_TAG_TYPES)); .catch(dispatchError(dispatch, ERROR_FETCH_TAG_TYPES));
}; };
} }
@ -38,7 +38,7 @@ export function createTagType({ name, description, icon }) {
return api return api
.create({ name, description, icon }) .create({ name, description, icon })
.then(() => dispatch({ type: ADD_TAG_TYPE, tagType: { name, description, icon } })) .then(() => dispatch({ type: ADD_TAG_TYPE, tagType: { name, description, icon } }))
.catch(dispatchAndThrow(dispatch, ERROR_CREATE_TAG_TYPE)); .catch(dispatchError(dispatch, ERROR_CREATE_TAG_TYPE));
}; };
} }
@ -48,7 +48,7 @@ export function updateTagType({ name, description, icon }) {
return api return api
.update({ name, description, icon }) .update({ name, description, icon })
.then(() => dispatch({ type: UPDATE_TAG_TYPE, tagType: { name, description, icon } })) .then(() => dispatch({ type: UPDATE_TAG_TYPE, tagType: { name, description, icon } }))
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_TAG_TYPE)); .catch(dispatchError(dispatch, ERROR_UPDATE_TAG_TYPE));
}; };
} }
@ -58,7 +58,7 @@ export function removeTagType(name) {
return api return api
.deleteTagType(name) .deleteTagType(name)
.then(() => dispatch({ type: DELETE_TAG_TYPE, tagType: { name } })) .then(() => dispatch({ type: DELETE_TAG_TYPE, tagType: { name } }))
.catch(dispatchAndThrow(dispatch, ERROR_DELETE_TAG_TYPE)); .catch(dispatchError(dispatch, ERROR_DELETE_TAG_TYPE));
}; };
} }

View File

@ -47,10 +47,11 @@ function deleteTagType(tagTypeName) {
}).then(throwIfNotSuccess); }).then(throwIfNotSuccess);
} }
export default { const api = {
fetchTagTypes, fetchTagTypes,
create, create,
update, update,
deleteTagType, deleteTagType,
validateTagType, validateTagType,
}; }
export default api;

View File

@ -1,8 +1,8 @@
import { List, Map as $Map } from 'immutable'; import { List, Map as $Map } from 'immutable';
const debug = require('debug')('unleash:tag-type-store');
import { RECEIVE_TAG_TYPES, ADD_TAG_TYPE, DELETE_TAG_TYPE, UPDATE_TAG_TYPE, ERROR_FETCH_TAG_TYPES } from './actions'; 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) => { const tagTypes = (state = new List([]), action) => {
switch (action.type) { switch (action.type) {
case ADD_TAG_TYPE: case ADD_TAG_TYPE:

View File

@ -1,5 +1,5 @@
import api from './api'; import api from './api';
import { dispatchAndThrow } from '../util'; import { dispatchError } from '../util';
export const START_FETCH_TAGS = 'START_FETCH_TAGS'; export const START_FETCH_TAGS = 'START_FETCH_TAGS';
export const RECEIVE_TAGS = 'RECEIVE_TAGS'; export const RECEIVE_TAGS = 'RECEIVE_TAGS';
@ -25,7 +25,7 @@ export function fetchTags() {
return api return api
.fetchTags() .fetchTags()
.then(json => dispatch(receiveTags(json))) .then(json => dispatch(receiveTags(json)))
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_TAGS)); .catch(dispatchError(dispatch, ERROR_FETCH_TAGS));
}; };
} }
@ -35,7 +35,7 @@ export function create({ type, value }) {
return api return api
.create({ type, value }) .create({ type, value })
.then(() => dispatch({ type: ADD_TAG, tag: { type, value } })) .then(() => dispatch({ type: ADD_TAG, tag: { type, value } }))
.catch(dispatchAndThrow(dispatch, ERROR_CREATE_TAG)); .catch(dispatchError(dispatch, ERROR_CREATE_TAG));
}; };
} }
@ -45,6 +45,6 @@ export function removeTag(tag) {
return api return api
.deleteTag(tag) .deleteTag(tag)
.then(() => dispatch({ type: DELETE_TAG, tag })) .then(() => dispatch({ type: DELETE_TAG, tag }))
.catch(dispatchAndThrow(dispatch, ERROR_DELETE_TAG)); .catch(dispatchError(dispatch, ERROR_DELETE_TAG));
}; };
} }

View File

@ -1,5 +1,5 @@
import api from './api'; import api from './api';
import { dispatchAndThrow } from '../util'; import { dispatchError } from '../util';
export const RECEIVE_CONFIG = 'RECEIVE_CONFIG'; export const RECEIVE_CONFIG = 'RECEIVE_CONFIG';
export const ERROR_RECEIVE_CONFIG = 'ERROR_RECEIVE_CONFIG'; export const ERROR_RECEIVE_CONFIG = 'ERROR_RECEIVE_CONFIG';
@ -14,5 +14,5 @@ export function fetchUIConfig() {
api api
.fetchConfig() .fetchConfig()
.then(json => dispatch(receiveConfig(json))) .then(json => dispatch(receiveConfig(json)))
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_CONFIG)); .catch(dispatchError(dispatch, ERROR_RECEIVE_CONFIG));
} }

View File

@ -1,13 +1,13 @@
import api from './api'; import api from "./api";
import { dispatchAndThrow } from '../util'; import { dispatchError } from "../util";
export const USER_CHANGE_CURRENT = 'USER_CHANGE_CURRENT'; export const USER_CHANGE_CURRENT = "USER_CHANGE_CURRENT";
export const USER_LOGOUT = 'USER_LOGOUT'; export const USER_LOGOUT = "USER_LOGOUT";
export const USER_LOGIN = 'USER_LOGIN'; export const USER_LOGIN = "USER_LOGIN";
export const START_FETCH_USER = 'START_FETCH_USER'; export const START_FETCH_USER = "START_FETCH_USER";
export const ERROR_FETCH_USER = 'ERROR_FETCH_USER'; export const ERROR_FETCH_USER = "ERROR_FETCH_USER";
const debug = require('debug')('unleash:user-actions'); const debug = require("debug")("unleash:user-actions");
const updateUser = value => ({ const updateUser = (value) => ({
type: USER_CHANGE_CURRENT, type: USER_CHANGE_CURRENT,
value, value,
}); });
@ -18,41 +18,44 @@ function handleError(error) {
export function fetchUser() { export function fetchUser() {
debug('Start fetching user'); debug('Start fetching user');
return dispatch => { return (dispatch) => {
dispatch({ type: START_FETCH_USER }); dispatch({ type: START_FETCH_USER });
return api return api
.fetchUser() .fetchUser()
.then(json => dispatch(updateUser(json))) .then((json) => dispatch(updateUser(json)))
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_USER)); .catch(dispatchError(dispatch, ERROR_FETCH_USER));
}; };
} }
export function insecureLogin(path, user) { export function insecureLogin(path, user) {
return dispatch => { return (dispatch) => {
dispatch({ type: START_FETCH_USER }); dispatch({ type: START_FETCH_USER });
return api return api
.insecureLogin(path, user) .insecureLogin(path, user)
.then(json => dispatch(updateUser(json))) .then((json) => dispatch(updateUser(json)))
.catch(handleError); .catch(handleError);
}; };
} }
export function passwordLogin(path, user) { export function passwordLogin(path, user) {
return dispatch => { return (dispatch) => {
dispatch({ type: START_FETCH_USER }); dispatch({ type: START_FETCH_USER });
return api return api
.passwordLogin(path, user) .passwordLogin(path, user)
.then(json => dispatch(updateUser(json))) .then((json) => dispatch(updateUser(json)))
.then(() => dispatch({ type: USER_LOGIN })); .then(() => dispatch({ type: USER_LOGIN }));
}; };
} }
export function logoutUser() { export function logoutUser() {
return dispatch => { return (dispatch) => {
dispatch({ type: USER_LOGOUT }); return api
window.location = 'logout'; .logoutUser()
.then(() => dispatch({ type: USER_LOGOUT }))
.then(() => window.location = "/")
.catch(handleError);
}; };
} }

View File

@ -1,39 +1,47 @@
import { throwIfNotSuccess, headers } from '../api-helper'; import { throwIfNotSuccess, headers } from "../api-helper";
const URI = 'api/admin/user'; const URI = "api/admin/user";
function logoutUser() { function logoutUser() {
return fetch(`${URI}/logout`, { method: 'GET', credentials: 'include' }) return fetch(`logout`, {
.then(throwIfNotSuccess) method: "GET",
.then(response => response.json()); credentials: "include",
}).then(throwIfNotSuccess);
} }
function fetchUser() { function fetchUser() {
return fetch(URI, { credentials: 'include' }) return fetch(URI, { credentials: "include" })
.then(throwIfNotSuccess) .then(throwIfNotSuccess)
.then(response => response.json()); .then((response) => response.json());
} }
function insecureLogin(path, user) { function insecureLogin(path, user) {
return fetch(path, { method: 'POST', credentials: 'include', headers, body: JSON.stringify(user) }) return fetch(path, {
method: "POST",
credentials: "include",
headers,
body: JSON.stringify(user),
})
.then(throwIfNotSuccess) .then(throwIfNotSuccess)
.then(response => response.json()); .then((response) => response.json());
} }
function passwordLogin(path, data) { function passwordLogin(path, data) {
return fetch(path, { return fetch(path, {
method: 'POST', method: "POST",
credentials: 'include', credentials: "include",
headers, headers,
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
.then(throwIfNotSuccess) .then(throwIfNotSuccess)
.then(response => response.json()); .then((response) => response.json());
} }
export default { const api = {
fetchUser, fetchUser,
insecureLogin, insecureLogin,
logoutUser, logoutUser,
passwordLogin, passwordLogin,
}; };
export default api;

View File

@ -14,6 +14,7 @@ const userStore = (state = new $Map(), action) => {
state = state.set('authDetails', action.error.body).set('showDialog', true); state = state.set('authDetails', action.error.body).set('showDialog', true);
return state; return state;
case USER_LOGOUT: case USER_LOGOUT:
console.log("Resetting state due to logout");
return new $Map(); return new $Map();
default: default:
return state; return state;

View File

@ -1,7 +1,7 @@
export const AUTH_REQUIRED = 'AUTH_REQUIRED'; export const AUTH_REQUIRED = 'AUTH_REQUIRED';
export const FORBIDDEN = 'FORBIDDEN'; export const FORBIDDEN = 'FORBIDDEN';
export function dispatchAndThrow(dispatch, type) { export function dispatchError(dispatch, type) {
return error => { return error => {
switch (error.statusCode) { switch (error.statusCode) {
case 401: case 401:
@ -14,7 +14,6 @@ export function dispatchAndThrow(dispatch, type) {
dispatch({ type, error, receivedAt: Date.now() }); dispatch({ type, error, receivedAt: Date.now() });
break; break;
} }
throw error;
}; };
} }

26
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

View File

@ -1,136 +0,0 @@
// docs: http://webpack.github.io/docs/configuration.html
'use strict';
const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const devMode = process.env.NODE_ENV !== 'production';
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
const entry = ['whatwg-fetch', './src/index'];
const plugins = [
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: 'bundle.css',
}),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development'),
},
}),
new webpack.optimize.ModuleConcatenationPlugin(),
new CleanWebpackPlugin(),
];
if (devMode) {
entry.push('webpack-dev-server/client?http://localhost:3000');
entry.push('webpack/hot/only-dev-server');
plugins.push(new webpack.HotModuleReplacementPlugin());
}
module.exports = {
mode,
entry,
resolve: {
extensions: ['.scss', '.css', '.js', '.jsx', '.json'],
},
output: {
path: path.join(__dirname, 'dist/public'),
filename: 'bundle.js',
publicPath: '/static/',
},
optimization: {
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
output: {
comments: false,
},
},
}),
new OptimizeCssAssetsPlugin({
cssProcessor: require('cssnano'),
cssProcessorOptions: { discardComments: { removeAll: true } },
canPrint: true,
}),
],
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
include: path.join(__dirname, 'src'),
},
{
test: /(\.scss)$/,
use: [
{
loader: devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
options: {
sourceMap: true,
modules: true,
importLoaders: 1,
localIdentName: '[name]__[local]___[hash:base64:5]',
},
},
{
loader: 'sass-loader',
options: {
// data: '@import "theme/_config.scss";',
includePaths: [path.resolve(__dirname, './src')],
},
},
],
},
{
test: /(\.css)$/,
use: [
{
loader: devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
},
{ loader: 'css-loader' },
],
},
],
},
plugins,
devtool: 'source-map',
devServer: {
proxy: {
'/api': {
target: process.env.UNLEASH_API || 'http://localhost:4242',
changeOrigin: true,
secure: false,
},
'/logout': {
target: process.env.UNLEASH_API || 'http://localhost:4242',
changeOrigin: true,
secure: false,
},
'/auth': {
target: process.env.UNLEASH_API || 'http://localhost:4242',
changeOrigin: true,
secure: false,
},
},
port: process.env.PORT || 3000,
host: '0.0.0.0',
disableHostCheck: true,
},
};

File diff suppressed because it is too large Load Diff