1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01: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:
node-version: ${{ matrix.node-version }}
- run: yarn
- run: yarn run test:ci
- run: yarn run test

1
frontend/.gitignore vendored
View File

@ -41,6 +41,7 @@ typings/
# Built
dist
build
# IDE
.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');
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",
"description": "unleash your features",
"version": "4.0.0-alpha.1",
"version": "3.15.1",
"keywords": [
"unleash",
"feature toggle",
@ -10,7 +10,7 @@
],
"files": [
"index.js",
"dist/"
"build/"
],
"repository": {
"type": "git",
@ -20,106 +20,93 @@
"url": "https://github.com/Unleash/unleash-frontend"
},
"engines": {
"node": ">=10"
"node": ">=12"
},
"license": "Apache-2.0",
"scripts": {
"build": "npm run build:assets && npm run build:html && npm run build:img && npm run build:ico",
"build:assets": "NODE_ENV=production webpack -p --display-optimization-bailout",
"build:html": "cp public/*.html dist/.",
"build:ico": "cp public/*.ico dist/.",
"build:img": "cp public/*.png dist/public/. && cp public/*.svg dist/public/.",
"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"
"build": "react-scripts build",
"start": "react-scripts start",
"start:heroku": "UNLEASH_API=https://unleash.herokuapp.com yarn run start",
"test": "react-scripts test",
"prepublish": "yarn run build"
},
"main": "./index.js",
"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",
"dependencies": {
"@material-ui/core": "^4.11.3",
"@material-ui/icons": "^4.11.2",
"@material-ui/styles": "^4.11.3",
"array-move": "^2.2.1",
"babel-eslint": "^10.1.0",
"babel-jest": "^25.3.0",
"babel-loader": "^8.1.0",
"classnames": "^2.2.6",
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^2.1.1",
"date-fns": "^2.17.0",
"debounce": "^1.2.0",
"debug": "^4.1.1",
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.11.0",
"enzyme-to-json": "^3.3.5",
"eslint": "^6.5.1",
"eslint-config-finn": "^3.0.1",
"eslint-config-finn-prettier": "^3.0.2",
"eslint-config-finn-react": "^2.0.2",
"eslint-plugin-react": "^7.11.1",
"@material-ui/lab": "^4.0.0-alpha.57",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/enzyme": "^3.10.8",
"@types/enzyme-adapter-react-16": "^1.0.6",
"@types/jest": "^26.0.15",
"@types/node": "^12.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-router-dom": "^5.1.7",
"array-move": "^3.0.1",
"classnames": "^2.3.1",
"css-loader": "^5.2.0",
"date-fns": "^2.19.0",
"debounce": "^1.2.1",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.6",
"fetch-mock": "^9.11.0",
"identity-obj-proxy": "^3.0.0",
"immutable": "^3.8.1",
"jest": "^26.6.3",
"immutable": "^4.0.0-rc.12",
"lodash.clonedeep": "^4.5.0",
"lodash.flow": "^3.5.0",
"mini-css-extract-plugin": "^0.9.0",
"node-fetch": "^2.6.1",
"node-sass": "^4.5.3",
"normalize.css": "^8.0.0",
"optimize-css-assets-webpack-plugin": "^5.0.0",
"prettier": "^1.18.2",
"prop-types": "^15.6.2",
"react": "^16.14.0",
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3",
"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",
"react": "^17.0.2",
"react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0",
"react-dom": "^17.0.2",
"react-redux": "^7.2.3",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-timeago": "^5.2.0",
"redux": "^4.0.5",
"redux-devtools": "^3.7.0",
"redux-devtools-extension": "^2.13.9",
"redux-mock-store": "^1.5.4",
"redux-thunk": "^2.3.0",
"sass-loader": "^7.0.1",
"style-loader": "^1.0.0",
"toolbox-loader": "0.0.3",
"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"
"sass": "^1.32.8",
"typescript": "^4.2.3",
"web-vitals": "^1.0.1"
},
"jest": {
"moduleNameMapper": {
"\\.(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"
},
"setupFilesAfterEnv": [
"<rootDir>/src/setupTests.js"
],
"setupFiles": [
"<rootDir>/jest-setup.js"
],
"snapshotSerializers": [
"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">
<head>
<meta charset="utf-8">
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="unleash">
<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/css?family=Roboto:300,400,500,700" rel="stylesheet">
</head>
<body>
<div id='app'></div>
<script src="public/bundle.js"></script>
</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(() => {
fetchFeatureToggles();
setSelectedProject(projects[0].id);
}, []);
}, [fetchFeatureToggles, projects]);
useEffect(() => {
setProjectOptions(formatProjectOptions(projects));
@ -43,7 +43,7 @@ const Reporting = ({ fetchFeatureToggles, projects }) => {
options={projectOptions}
value={selectedProject}
onChange={onChange}
inputProps={{ ['data-test']: REPORTING_SELECT_ID }}
inputProps={{ ['data-testid']: REPORTING_SELECT_ID }}
/>
</div>
);

View File

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

View File

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

View File

@ -22,17 +22,17 @@ const AddonFormComponent = ({ editMode, provider, addon, fetch, cancel, submit }
if (!provider) {
fetch();
}
}, []); // empty array => fetch only first time
}, [fetch, provider]); // empty array => fetch only first time
useEffect(() => {
setConfig({ ...addon });
}, [addon.id]);
}, [addon]);
useEffect(() => {
if (provider && !config.provider) {
setConfig({ ...addon, provider: provider.name });
}
}, [provider]);
}, [provider, addon, config.provider]);
const setFieldValue = field => evt => {
evt.preventDefault();
@ -104,7 +104,7 @@ const AddonFormComponent = ({ editMode, provider, addon, fetch, cancel, submit }
<PageContent headerContent={`Configure ${name} addon`}>
<section className={styles.formSection}>
{description}&nbsp;
<a href={documentationUrl} target="_blank">
<a href={documentationUrl} target="_blank" rel="noreferrer">
Read more
</a>
<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 React, { useRef } from 'react';
const RNDContext = createDndContext(HTML5Backend);
import React from 'react';
function useDNDProviderElement(props) {
const manager = useRef(RNDContext);
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) {

View File

@ -108,7 +108,7 @@ export function getIcon(type) {
}
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>
</a>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -499,6 +499,7 @@ exports[`renders correctly with with variants 1`] = `
<a
href="https://unleash.github.io/docs/toggle_variants"
rel="noreferrer"
target="_blank"
>
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 { 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 MySelect from '../../common/select';
import { trim, modalStyles } from '../../common/util';
import { modalStyles, trim } from '../../common/util';
import { weightTypes } from './enums';
import OverrideConfig from './e-override-config';
@ -46,6 +46,7 @@ function AddVariant({ showDialog, closeDialog, save, validateName, editVariant,
useEffect(() => {
clear();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editVariant]);
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
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
</a>
</small>

View File

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

View File

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

View File

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

View File

@ -43,7 +43,7 @@ function renderLink(link, toggleDrawer) {
key={link.href}
target="_blank"
className={[styles.navigationLink].join(' ')}
title={link.title}
title={link.title} rel="noreferrer"
>
{getIcon(link.icon)} {link.value}
</a>
@ -56,7 +56,7 @@ export const DrawerMenu = ({ links = [], title = 'Unleash', flags = {}, open = f
<div className={styles.drawerContainer}>
<div className={styles.drawerTitleContainer}>
<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>
</div>

View File

@ -15,7 +15,7 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history, hasPermi
const styles = useStyles();
useEffect(() => {
fetchProjects();
}, []);
}, [fetchProjects]);
const addProjectButton = () => (
<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 {
Avatar,
Button,
Card,
CardHeader,
Avatar,
List,
ListItem,
ListItemSecondaryAction,
ListItemText,
ListItemAvatar,
Select,
MenuItem,
Icon,
IconButton,
Dialog,
DialogActions,
DialogTitle,
DialogContentText,
DialogContent,
Button,
DialogContentText,
DialogTitle,
Icon,
IconButton,
List,
ListItem,
ListItemAvatar,
ListItemSecondaryAction,
ListItemText,
MenuItem,
Select,
} from '@material-ui/core';
import AddUserComponent from './access-add-user';
@ -38,6 +38,7 @@ function AccessComponent({ projectId, project }) {
useEffect(() => {
fetchAccess();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]);
if (!project) {

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import classnames from 'classnames';
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 { useHistory } from 'react-router';
import { useCommonStyles } from '../../../common.styles';
@ -121,9 +121,9 @@ const PasswordAuth = ({ authDetails, passwordLogin, loadInitialData }) => {
condition={showFields}
show={renderLoginForm()}
elseShow={
<a href="" onClick={onShowOptions}>
<IconButton> onClick={onShowOptions}>
Show more options
</a>
</IconButton>
}
/>
</div>

View File

@ -29,7 +29,7 @@ const SimpleAuth = ({ insecureLogin, loadInitialData, history, authDetails }) =>
<p>
This instance of Unleash is not set up with a secure authentication provider. You can read more
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
</a>
</p>

View File

@ -1,25 +1,24 @@
import React from 'react';
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Card, CardContent, CardHeader } from '@material-ui/core';
import { styles as commonStyles } from '../common';
export default class FeatureListComponent extends React.Component {
static propTypes = {
logoutUser: PropTypes.func.isRequired,
};
const LogoutComponent = ({logoutUser}) => {
useEffect(() => {
logoutUser();
});
componentDidMount() {
this.props.logoutUser();
}
render() {
return (
<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>
);
}
return (<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 locale = this.getLocale();
let foundLocale = this.possibleLocales.find(l => l.value === locale);
const imageUrl = email ? this.props.profile.imageUrl : 'public/unknown-user.png';
const imageLocale = foundLocale ? `public/${foundLocale.image}.png` : `public/unknown-locale.png`;
const imageUrl = email ? this.props.profile.imageUrl : 'unknown-user.png';
const imageLocale = foundLocale ? `${foundLocale.image}.png` : `unknown-locale.png`;
return (
<div className={styles.showUserSettings}>
<DropdownMenu
@ -60,7 +60,7 @@ export default class ShowUserComponent extends React.Component {
this.possibleLocales.map(i => (
<MenuItem key={i.value} onClick={() => this.setLocale(i)}>
<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>
</div>
</MenuItem>

View File

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

View File

@ -12,7 +12,7 @@ function ApiHowTo() {
}}
>
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
</a>{' '}
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(() => {
fetchApiKeys();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Icon } from '@material-ui/core';
import { Icon, IconButton } from '@material-ui/core';
function Secret({ value }) {
const [show, setShow] = useState(false);
@ -17,9 +17,9 @@ function Secret({ value }) {
<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>
</a>
</IconButton>
</div>
);
}

View File

@ -15,6 +15,7 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission
useEffect(() => {
getGoogleConfig();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
@ -59,7 +60,7 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission
<Grid item xs={12}>
<Typography variant="subtitle1">
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
</a>{' '}
to learn how to integrate with Google OAuth 2.0. <br />

View File

@ -15,12 +15,14 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) {
useEffect(() => {
getSamlConfig();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (config.entityId) {
setData(config);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]);
if (!hasPermission('ADMIN')) {
@ -59,7 +61,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) {
<Grid item md={12}>
<Typography variant="subtitle1">
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
</a>{' '}
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 */
import React, { useEffect, useState } from 'react';
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 AddUser from '../add-user-component';
import ChangePassword from '../change-password-component';
@ -65,6 +65,7 @@ function UsersList({
useEffect(() => {
fetchUsers();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
@ -92,22 +93,22 @@ function UsersList({
condition={hasPermission('ADMIN')}
show={
<TableCell>
<a href="" title="Edit" onClick={openUpdateDialog(item)}>
<IconButton aria-label="Edit" title="Edit" onClick={openUpdateDialog(item)}>
<Icon>edit</Icon>
</a>
<a href="" title="Change password" onClick={openPwDialog(item)}>
</IconButton>
<IconButton aria-label="Change password" title="Change password" onClick={openPwDialog(item)}>
<Icon>lock</Icon>
</a>
<a href="" title="Remove user" onClick={openDelDialog(item)}>
</IconButton>
<IconButton aria-label="Remove user" title="Remove user" onClick={openDelDialog(item)}>
<Icon>delete</Icon>
</a>
</IconButton>
</TableCell>
}
elseShow={
<TableCell>
<a href="" title="Change password" onClick={openPwDialog(item)}>
<IconButton aria-label="Change password" title="Change password" onClick={openPwDialog(item)}>
<Icon>lock</Icon>
</a>
</IconButton>
</TableCell>
}
/>

View File

@ -13,12 +13,13 @@ import {
import { showPermissions, modalStyles } from './util';
function AddUser({ user, showDialog, closeDialog, updateUser }) {
const [data, setData] = useState(user);
const [error, setError] = useState({});
if (!user) {
return null;
}
const [data, setData] = useState(user);
const [error, setError] = useState({});
const updateField = e => {
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 Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
process.env.TZ = 'UTC';
configure({ adapter: new Adapter() });

View File

@ -1,6 +1,6 @@
import reducer from '../index';
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';
test('should be default state', () => {

View File

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

View File

@ -1,5 +1,5 @@
import api from './api';
import { dispatchAndThrow } from '../util';
import { dispatchError } from '../util';
import { MUTE_ERROR } from '../error/actions';
export const RECEIVE_ALL_APPLICATIONS = 'RECEIVE_ALL_APPLICATIONS';
@ -26,7 +26,7 @@ export function fetchAll() {
api
.fetchAll()
.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) {
@ -38,7 +38,7 @@ export function storeApplicationMetaData(appName, key, value) {
setTimeout(() => dispatch({ type: MUTE_ERROR, error: info }), 1000);
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) {
@ -46,7 +46,7 @@ export function fetchApplication(appName) {
api
.fetchApplication(appName)
.then(json => dispatch(recieveApplication(json)))
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_ALL_APPLICATIONS));
.catch(dispatchError(dispatch, ERROR_RECEIVE_ALL_APPLICATIONS));
}
export function deleteApplication(appName) {
@ -54,5 +54,5 @@ export function deleteApplication(appName) {
api
.deleteApplication(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 { dispatchAndThrow } from '../util';
import { dispatchError } from '../util';
export const REVIVE_TOGGLE = 'REVIVE_TOGGLE';
export const RECEIVE_ARCHIVE = 'RECEIVE_ARCHIVE';
@ -20,7 +20,7 @@ export function revive(featureToggle) {
api
.revive(featureToggle)
.then(() => dispatch(reviveToggle(featureToggle)))
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_ARCHIVE));
.catch(dispatchError(dispatch, ERROR_RECEIVE_ARCHIVE));
}
export function fetchArchive() {
@ -28,5 +28,5 @@ export function fetchArchive() {
api
.fetchAll()
.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 { dispatchAndThrow } from '../util';
import { dispatchError } from '../util';
export const RECEIVE_CONTEXT = '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);
dispatch(receiveContext(json));
})
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_CONTEXT));
.catch(dispatchError(dispatch, ERROR_RECEIVE_CONTEXT));
}
export function removeContextField(context) {
@ -31,7 +31,7 @@ export function removeContextField(context) {
api
.remove(context)
.then(() => dispatch(createRemoveContext(context)))
.catch(dispatchAndThrow(dispatch, ERROR_REMOVING_CONTEXT));
.catch(dispatchError(dispatch, ERROR_REMOVING_CONTEXT));
}
export function createContextField(context) {
@ -39,7 +39,7 @@ export function createContextField(context) {
api
.create(context)
.then(() => dispatch(addContextField(context)))
.catch(dispatchAndThrow(dispatch, ERROR_ADD_CONTEXT_FIELD));
.catch(dispatchError(dispatch, ERROR_ADD_CONTEXT_FIELD));
}
export function updateContextField(context) {
@ -47,7 +47,7 @@ export function updateContextField(context) {
api
.update(context)
.then(() => dispatch(upContextField(context)))
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_CONTEXT_FIELD));
.catch(dispatchError(dispatch, ERROR_UPDATE_CONTEXT_FIELD));
}
export function validateName(name) {

View File

@ -1,5 +1,5 @@
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_ERROR = 'RECIEVE_GOOGLE_CONFIG_ERROR';
export const UPDATE_GOOGLE_AUTH = 'UPDATE_GOOGLE_AUTH';
@ -22,7 +22,7 @@ export function getGoogleConfig() {
config,
})
)
.catch(dispatchAndThrow(dispatch, RECIEVE_GOOGLE_CONFIG_ERROR));
.catch(dispatchError(dispatch, RECIEVE_GOOGLE_CONFIG_ERROR));
}
export function updateGoogleConfig(data) {
@ -30,7 +30,7 @@ export function updateGoogleConfig(data) {
api
.updateGoogleConfig(data)
.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() {
@ -44,7 +44,7 @@ export function getSamlConfig() {
config,
})
)
.catch(dispatchAndThrow(dispatch, RECIEVE_SAML_CONFIG_ERROR));
.catch(dispatchError(dispatch, RECIEVE_SAML_CONFIG_ERROR));
}
export function updateSamlConfig(data) {
@ -52,5 +52,5 @@ export function updateSamlConfig(data) {
api
.updateSamlConfig(data)
.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 { dispatchAndThrow } from '../util';
import { dispatchError } from '../util';
export const RECIEVE_KEYS = 'RECIEVE_KEYS';
export const ERROR_FETCH_KEYS = 'ERROR_FETCH_KEYS';
export const REMOVE_KEY = 'REMOVE_KEY';
@ -20,7 +20,7 @@ export function fetchApiKeys() {
tokens: value.tokens,
})
)
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_KEYS));
.catch(dispatchError(dispatch, ERROR_FETCH_KEYS));
}
export function removeKey(secret) {
@ -28,7 +28,7 @@ export function removeKey(secret) {
api
.remove(secret)
.then(() => dispatch({ type: REMOVE_KEY, secret }))
.catch(dispatchAndThrow(dispatch, REMOVE_KEY));
.catch(dispatchError(dispatch, REMOVE_KEY));
}
export function addKey(data) {
@ -36,5 +36,5 @@ export function addKey(data) {
api
.create(data)
.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 { dispatchAndThrow } from '../util';
import { dispatchError } from '../util';
export const START_FETCH_USERS = 'START_FETCH_USERS';
export const RECIEVE_USERS = 'RECIEVE_USERS';
export const ERROR_FETCH_USERS = 'ERROR_FETCH_USERS';
@ -27,7 +27,7 @@ export function fetchUsers() {
return api
.fetchAll()
.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
.remove(user)
.then(() => dispatch({ type: REMOVE_USER, user }))
.catch(dispatchAndThrow(dispatch, REMOVE_USER_ERROR));
.catch(dispatchError(dispatch, REMOVE_USER_ERROR));
}
export function addUser(user) {
@ -44,7 +44,7 @@ export function addUser(user) {
api
.create(user)
.then(newUser => dispatch({ type: ADD_USER, user: newUser }))
.catch(dispatchAndThrow(dispatch, ADD_USER_ERROR));
.catch(dispatchError(dispatch, ADD_USER_ERROR));
}
export function updateUser(user) {
@ -52,13 +52,13 @@ export function updateUser(user) {
api
.update(user)
.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) {
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) {
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 { dispatchAndThrow } from '../util';
import { dispatchError } from '../util';
export const TAG_FEATURE_TOGGLE = 'TAG_FEATURE_TOGGLE';
export const UNTAG_FEATURE_TOGGLE = 'UNTAG_FEATURE_TOGGLE';
@ -25,7 +25,7 @@ export function tagFeature(featureToggle, tag) {
return api
.tagFeature(featureToggle, tag)
.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
.untagFeature(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
.fetchFeatureToggleTags(featureToggle)
.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';
const debug = require('debug')('unleash:feature-actions');
import { dispatchAndThrow } from '../util';
import { dispatchError } from '../util';
import { MUTE_ERROR } from '../error/actions';
const debug = require('debug')('unleash:feature-actions');
export const ADD_FEATURE_TOGGLE = 'ADD_FEATURE_TOGGLE';
export const COPY_FEATURE_TOGGLE = 'COPY_FEATURE_TOGGLE';
@ -77,7 +77,7 @@ export function fetchFeatureToggles() {
})
.catch(() => {
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
.fetchFeatureToggle(name)
.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,
});
})
.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
.toggle(enable, 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);
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);
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,
});
})
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
.catch(dispatchError(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
};
}
@ -189,7 +189,7 @@ export function requestUpdateFeatureToggleVariants(featureToggle, newVariants) {
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
.remove(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';
const debug = require('debug')('unleash:feature-store');
import {
ADD_FEATURE_TOGGLE,
RECEIVE_FEATURE_TOGGLES,
@ -13,6 +11,8 @@ import {
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
const debug = require('debug')('unleash:feature-store');
const features = (state = new List([]), action) => {
switch (action.type) {
case ADD_FEATURE_TOGGLE:

View File

@ -1,5 +1,5 @@
import api from './api';
import { dispatchAndThrow } from '../util';
import { dispatchError } from '../util';
export const RECEIVE_FEATURE_TYPES = 'RECEIVE_FEATURE_TYPES';
export const ERROR_RECEIVE_FEATURE_TYPES = 'ERROR_RECEIVE_FEATURE_TYPES';
@ -11,5 +11,5 @@ export function fetchFeatureTypes() {
api
.fetchAll()
.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 { dispatchAndThrow } from '../util';
import { dispatchError } from '../util';
export const RECEIVE_HISTORY = 'RECEIVE_HISTORY';
export const ERROR_RECEIVE_HISTORY = 'ERROR_RECEIVE_HISTORY';
@ -21,7 +21,7 @@ export function fetchHistory() {
api
.fetchAll()
.then(json => dispatch(receiveHistory(json)))
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_HISTORY));
.catch(dispatchError(dispatch, ERROR_RECEIVE_HISTORY));
}
export function fetchHistoryForToggle(toggleName) {
@ -29,5 +29,5 @@ export function fetchHistoryForToggle(toggleName) {
api
.fetchHistoryForToggle(toggleName)
.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 { dispatchAndThrow } from '../util';
import { dispatchError } from '../util';
export const RECEIVE_PROJECT = 'RECEIVE_PROJECT';
export const ERROR_RECEIVE_PROJECT = 'ERROR_RECEIVE_PROJECT';
@ -22,7 +22,7 @@ export function fetchProjects() {
.then(json => {
dispatch(receiveProjects(json.projects));
})
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_PROJECT));
.catch(dispatchError(dispatch, ERROR_RECEIVE_PROJECT));
}
export function removeProject(project) {
@ -30,7 +30,7 @@ export function removeProject(project) {
api
.remove(project)
.then(() => dispatch(delProject(project)))
.catch(dispatchAndThrow(dispatch, ERROR_REMOVING_PROJECT));
.catch(dispatchError(dispatch, ERROR_REMOVING_PROJECT));
}
export function createProject(project) {
@ -38,7 +38,7 @@ export function createProject(project) {
api
.create(project)
.then(() => dispatch(addProject(project)))
.catch(dispatchAndThrow(dispatch, ERROR_ADD_PROJECT));
.catch(dispatchError(dispatch, ERROR_ADD_PROJECT));
}
export function updateProject(project) {
@ -46,7 +46,7 @@ export function updateProject(project) {
api
.update(project)
.then(() => dispatch(upProject(project)))
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_PROJECT));
.catch(dispatchError(dispatch, ERROR_UPDATE_PROJECT));
}
export function validateId(id) {

View File

@ -1,6 +1,6 @@
import api from './api';
import applicationApi from '../application/api';
import { dispatchAndThrow } from '../util';
import { dispatchError } from '../util';
export const ADD_STRATEGY = 'ADD_STRATEGY';
export const UPDATE_STRATEGY = 'UPDATE_STRATEGY';
@ -44,7 +44,7 @@ export function fetchStrategies() {
return api
.fetchAll()
.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
.create(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
.update(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
.remove(strategy)
.then(() => dispatch(createRemoveStrategy(strategy)))
.catch(dispatchAndThrow(dispatch, ERROR_REMOVING_STRATEGY));
.catch(dispatchError(dispatch, ERROR_REMOVING_STRATEGY));
}
export function getApplicationsWithStrategy(strategyName) {
@ -87,7 +87,7 @@ export function deprecateStrategy(strategy) {
dispatch(startDeprecate());
api.deprecate(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());
api.reactivate(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 { dispatchAndThrow } from '../util';
import { dispatchError } from '../util';
export const START_FETCH_TAG_TYPES = 'START_FETCH_TAG_TYPES';
export const RECEIVE_TAG_TYPES = 'RECEIVE_TAG_TYPES';
@ -28,7 +28,7 @@ export function fetchTagTypes() {
return api
.fetchTagTypes()
.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
.create({ 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
.update({ 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
.deleteTagType(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);
}
export default {
const api = {
fetchTagTypes,
create,
update,
deleteTagType,
validateTagType,
};
}
export default api;

View File

@ -1,8 +1,8 @@
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';
const debug = require('debug')('unleash:tag-type-store');
const tagTypes = (state = new List([]), action) => {
switch (action.type) {
case ADD_TAG_TYPE:

View File

@ -1,5 +1,5 @@
import api from './api';
import { dispatchAndThrow } from '../util';
import { dispatchError } from '../util';
export const START_FETCH_TAGS = 'START_FETCH_TAGS';
export const RECEIVE_TAGS = 'RECEIVE_TAGS';
@ -25,7 +25,7 @@ export function fetchTags() {
return api
.fetchTags()
.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
.create({ 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
.deleteTag(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 { dispatchAndThrow } from '../util';
import { dispatchError } from '../util';
export const RECEIVE_CONFIG = 'RECEIVE_CONFIG';
export const ERROR_RECEIVE_CONFIG = 'ERROR_RECEIVE_CONFIG';
@ -14,5 +14,5 @@ export function fetchUIConfig() {
api
.fetchConfig()
.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 { dispatchAndThrow } from '../util';
export const USER_CHANGE_CURRENT = 'USER_CHANGE_CURRENT';
export const USER_LOGOUT = 'USER_LOGOUT';
export const USER_LOGIN = 'USER_LOGIN';
export const START_FETCH_USER = 'START_FETCH_USER';
export const ERROR_FETCH_USER = 'ERROR_FETCH_USER';
const debug = require('debug')('unleash:user-actions');
import api from "./api";
import { dispatchError } from "../util";
export const USER_CHANGE_CURRENT = "USER_CHANGE_CURRENT";
export const USER_LOGOUT = "USER_LOGOUT";
export const USER_LOGIN = "USER_LOGIN";
export const START_FETCH_USER = "START_FETCH_USER";
export const ERROR_FETCH_USER = "ERROR_FETCH_USER";
const debug = require("debug")("unleash:user-actions");
const updateUser = value => ({
const updateUser = (value) => ({
type: USER_CHANGE_CURRENT,
value,
});
@ -18,41 +18,44 @@ function handleError(error) {
export function fetchUser() {
debug('Start fetching user');
return dispatch => {
return (dispatch) => {
dispatch({ type: START_FETCH_USER });
return api
.fetchUser()
.then(json => dispatch(updateUser(json)))
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_USER));
.then((json) => dispatch(updateUser(json)))
.catch(dispatchError(dispatch, ERROR_FETCH_USER));
};
}
export function insecureLogin(path, user) {
return dispatch => {
return (dispatch) => {
dispatch({ type: START_FETCH_USER });
return api
.insecureLogin(path, user)
.then(json => dispatch(updateUser(json)))
.then((json) => dispatch(updateUser(json)))
.catch(handleError);
};
}
export function passwordLogin(path, user) {
return dispatch => {
return (dispatch) => {
dispatch({ type: START_FETCH_USER });
return api
.passwordLogin(path, user)
.then(json => dispatch(updateUser(json)))
.then((json) => dispatch(updateUser(json)))
.then(() => dispatch({ type: USER_LOGIN }));
};
}
export function logoutUser() {
return dispatch => {
dispatch({ type: USER_LOGOUT });
window.location = 'logout';
return (dispatch) => {
return api
.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() {
return fetch(`${URI}/logout`, { method: 'GET', credentials: 'include' })
.then(throwIfNotSuccess)
.then(response => response.json());
return fetch(`logout`, {
method: "GET",
credentials: "include",
}).then(throwIfNotSuccess);
}
function fetchUser() {
return fetch(URI, { credentials: 'include' })
return fetch(URI, { credentials: "include" })
.then(throwIfNotSuccess)
.then(response => response.json());
.then((response) => response.json());
}
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(response => response.json());
.then((response) => response.json());
}
function passwordLogin(path, data) {
return fetch(path, {
method: 'POST',
credentials: 'include',
method: "POST",
credentials: "include",
headers,
body: JSON.stringify(data),
})
.then(throwIfNotSuccess)
.then(response => response.json());
.then((response) => response.json());
}
export default {
const api = {
fetchUser,
insecureLogin,
logoutUser,
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);
return state;
case USER_LOGOUT:
console.log("Resetting state due to logout");
return new $Map();
default:
return state;

View File

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