Introduce redux store
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Armin Friedl 2020-07-19 16:12:59 +02:00
parent 0c1fe8efce
commit 41f2a22f3d
Signed by: armin
GPG key ID: 48C726EEE7FBCBC8
21 changed files with 365 additions and 156 deletions

32
examples/python/fling.py Normal file
View file

@ -0,0 +1,32 @@
import flingclient as fc
from flingclient.rest import ApiException
from datetime import datetime
# Per default the dockerized fling service runs on localhost:3000 In case you
# run your own instance, change the base url
configuration = fc.Configuration(host="http://localhost:3000")
# Every call, with the exception of `/api/auth`, is has to be authorized by a
# bearer token. Get a token by authenticating as admin and set it into the
# configuration. All subsequent calls will send this token in the header as
# `Authorization: Bearer <token> header`
def authenticate(admin_user, admin_password):
with fc.ApiClient(configuration) as api_client:
auth_client = fc.AuthApi(api_client)
admin_auth = fc.AdminAuth(admin_user, admin_password)
configuration.access_token = auth_client.authenticate_owner(admin_auth=admin_auth)
admin_user = input("Username: ")
admin_password = input("Password: ")
authenticate(admin_user, admin_password)
with fc.ApiClient(configuration) as api_client:
# Create a new fling
fling_client = fc.FlingApi(api_client)
fling = fc.Fling(name="A Fling from Python", auth_code="secret",
direct_download=False, allow_upload=True,
expiration_time=datetime(2099, 12, 12))
fling = fling_client.post_fling()
print(f"Created a new fling: {fling}")
#

39
examples/querysheet.http Normal file
View file

@ -0,0 +1,39 @@
######################################
# Fling Querysheet for restclient.el #
######################################
# Authenticate as user
POST http://localhost:8080/api/auth/user
{"shareId": "shareId", "authCode":"secret"}
-> jq-set-var :token .
# :token = Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTQ0NjEzNzMsImV4cCI6MTU5NDY0MTM3Mywic3ViIjoiYWRtaW4ifQ.yu6sF1aE6sW4Jx1hBMj6iUsy8xfiaRGlIFVnHK4YkU8
# Authenticate as admin
POST http://localhost:8080/api/auth/admin
Content-Type: application/json
{"adminName": "admin", "adminPassword":"123"}
-> run-hook (restclient-set-var ":token" (buffer-substring-no-properties 1 (line-end-position)))
# Get all flings
GET http://localhost:8080/api/fling
Authorization: Bearer :token
#
:flingId = dfc208a3-5924-43b4-aa6a-c263541dca5e
# Get one fling
GET http://localhost:8080/api/fling/:flingId
:token
# Get all artifacts
GET http://localhost:8080/api/fling/:flingId/artifacts
:token
#
GET https://httpbin.org/json
-> jq-set-var :my-var .slideshow.slides[0].title
#
GET http://httpbin.org/ip
-> run-hook (restclient-set-var ":my-ip" (cdr (assq 'origin (json-read))))

View file

@ -6740,9 +6740,9 @@
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
}, },
"immer": { "immer": {
"version": "1.10.0", "version": "7.0.5",
"resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz", "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.5.tgz",
"integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==" "integrity": "sha512-TtRAKZyuqld2eYjvWgXISLJ0ZlOl1OOTzRmrmiY8SlB0dnAhZ1OiykIDL5KDFNaPHDXiLfGQFNJGtet8z8AEmg=="
}, },
"import-cwd": { "import-cwd": {
"version": "2.1.0", "version": "2.1.0",
@ -11512,6 +11512,11 @@
} }
} }
}, },
"immer": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz",
"integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg=="
},
"inquirer": { "inquirer": {
"version": "7.0.4", "version": "7.0.4",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz",
@ -11922,6 +11927,11 @@
"symbol-observable": "^1.0.2" "symbol-observable": "^1.0.2"
} }
}, },
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
},
"regenerate": { "regenerate": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",

View file

@ -10,6 +10,7 @@
"axios": "^0.19.2", "axios": "^0.19.2",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"immer": "^7.0.5",
"jwt-decode": "^2.2.0", "jwt-decode": "^2.2.0",
"loglevel": "^1.6.8", "loglevel": "^1.6.8",
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
@ -20,6 +21,7 @@
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-scripts": "3.4.1", "react-scripts": "3.4.1",
"redux": "^4.0.5", "redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"spectre.css": "^0.5.8" "spectre.css": "^0.5.8"
}, },
"scripts": { "scripts": {

View file

@ -1,7 +1,7 @@
import log from 'loglevel'; import log from 'loglevel';
import React from 'react'; import React from 'react';
import {Switch, Route, Redirect} from "react-router-dom"; import { Switch, Route, Redirect } from "react-router-dom";
import jwt from './util/jwt.js'; import jwt from './util/jwt.js';
@ -12,6 +12,19 @@ import Unlock from './components/user/Unlock';
import FlingUser from './components/user/FlingUser'; import FlingUser from './components/user/FlingUser';
import LandingPage from './components/LandingPage'; import LandingPage from './components/LandingPage';
/**
* Front routes, defaults to a 404 Page.
* Routes:
* - / : Landing page
* - /admin/login : A login page. Redirects with admin token upon successful
login
* - /admin : The fling administration page. Redirects to a login page if not
authenticated
* - /admin/[fling id]/* : Go directly to a fling (sub-)page. Redirects to a
login page if not authenticated
* - /unlock : A unlock page. Redirects with user token upon successful login.
* - /f/[shareId] : Opens a fling page for a user
*/
export default () => { export default () => {
return ( return (
<Switch> <Switch>
@ -19,7 +32,7 @@ export default () => {
<Route exact path="/admin/login" component={Login} /> <Route exact path="/admin/login" component={Login} />
<OwnerRoute exact path="/admin"><FlingAdmin /></OwnerRoute> <OwnerRoute exact path="/admin"><FlingAdmin /></OwnerRoute>
<OwnerRoute path="/admin/:fling"><FlingAdmin /></OwnerRoute> <OwnerRoute path="/admin/:flingId"><FlingAdmin /></OwnerRoute>
<Route exact path="/unlock" component={Unlock} /> <Route exact path="/unlock" component={Unlock} />
<UserRoute path="/f/:shareId"><FlingUser /></UserRoute> <UserRoute path="/f/:shareId"><FlingUser /></UserRoute>
@ -45,14 +58,19 @@ function OwnerRoute({ children, ...rest }) {
{...rest} {...rest}
render={({ location }) => { render={({ location }) => {
if (jwt.hasSubject("admin")) { return children; } if (jwt.hasSubject("admin")) { return children; }
else { return <Redirect to={{pathname: "/admin/login", state: {from: location}}} />; } else {
return <Redirect to={{
pathname: "/admin/login",
state: { from: location }
}} />;
}
}} }}
/> />
); );
} }
/* A wrapper for <Route> that redirects to the unlock screen if no authorized token /* A wrapper for <Route> that redirects to the unlock screen if no authorized
* was found. * token * was found.
* *
* Note that the token check is purely client-side. It provides no actual * Note that the token check is purely client-side. It provides no actual
* protection! It is hence possible to reach the target site with some small * protection! It is hence possible to reach the target site with some small
@ -65,11 +83,14 @@ function UserRoute({ children, ...rest }) {
<Route <Route
{...rest} {...rest}
render={({ match, location }) => { render={({ match, location }) => {
let state = {from: location, shareId: match.params.shareId}; let state = { from: location, shareId: match.params.shareId };
let authorized = jwt.hasSubject("admin") || (jwt.hasSubject("user") && jwt.hasClaim("id", state['shareId']));
let authorized =
jwt.hasSubject("admin")
|| ( jwt.hasSubject("user") && jwt.hasClaim("id", state['shareId']) );
if (authorized) { return children; } if (authorized) { return children; }
else { return <Redirect to={ {pathname: "/unlock", state: state} } />; } else { return <Redirect to={{ pathname: "/unlock", state: state }} />; }
}} }}
/> />
); );

View file

@ -13,7 +13,6 @@ export default function LandingPage() {
function openFling(ev) { function openFling(ev) {
ev.preventDefault(); ev.preventDefault();
window.location = `/f/${shareId}`; window.location = `/f/${shareId}`;
} }

View file

@ -1,23 +0,0 @@
import React from 'react';
export default (props) => {
function renderError() {
return (
<div className="toast toast-error mb-2">
<button className="btn btn-clear float-right" onClick={props.clearErrors}></button>
<h5>Ooops!</h5>
<li>
{ props.errors.map( (err, idx) => <ul key={idx}>{err}</ul> ) }
</li>
</div>
);
}
return (
<>
{ props.errors.length > 0 && !props.below ? renderError() : "" }
{ props.children }
{ props.errors.length > 0 && props.below ? renderError() : "" }
</>
);
}

View file

@ -1,22 +1,39 @@
import React from 'react'; import React, { useEffect } from 'react';
import { useDispatch } from "react-redux";
import { useParams } from 'react-router-dom';
import { retrieveFlings, setActiveFling } from "../../redux/actions";
import Navbar from './Navbar'; import Navbar from './Navbar';
import FlingList from './FlingList'; import FlingList from './FlingList';
import FlingContent from './FlingContent'; import FlingContent from './FlingContent';
import {useParams} from 'react-router-dom';
export default function FlingAdmin() { export default function FlingAdmin() {
let { fling } = useParams(); const { flingId } = useParams();
const dispatch = useDispatch();
return( useEffect(() => {
dispatch(retrieveFlings());
}, [dispatch]);
useEffect(() => {
if (flingId) {
dispatch(setActiveFling(flingId))
}
}, [flingId, dispatch]);
return (
<div> <div>
<Navbar /> <Navbar />
<div className="container"> <div className="container">
<div className="columns mt-2"> <div className="columns mt-2">
<div className="column col-sm-12 col-lg-3 col-2"> <FlingList activeFling={fling} /> </div> <div className="column col-sm-12 col-lg-3 col-2">
<div className="column col-sm-12"><FlingContent activeFling={fling} /></div> <FlingList />
</div>
<div className="column col-sm-12">
<FlingContent />
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,33 +1,18 @@
import log from 'loglevel'; import React from 'react';
import React, {useState, useEffect} from 'react'; import { useSelector } from "react-redux";
import {FlingClient} from '../../util/fc';
import FlingTile from './FlingTile'; import FlingTile from './FlingTile';
export default function FlingList(props) { export default function FlingList() {
const [flings, setFlings] = useState([]); const flings = useSelector((store) => store.flings.flings);
useEffect(() => {
let flingClient = new FlingClient();
flingClient.getFlings()
.then(flings => {
let newFlings = [];
for (let fling of flings) {
let flingTile = <FlingTile fling={fling} key={fling.id} />;
newFlings.push(flingTile);
}
setFlings(newFlings);
}).catch(log.error);
}, []);
return( return (
<div className="panel"> <div className="panel">
{log.info(`Got active fling: ${props.activeFling}`)}
<div className="panel-header p-2"> <div className="panel-header p-2">
<h5>My Flings</h5> <h5>My Flings</h5>
</div> </div>
<div className="panel-body p-0"> <div className="panel-body p-0">
{flings} {flings.map(fling => <FlingTile fling={fling} key={fling.id} />)}
</div> </div>
</div> </div>
); );

View file

@ -1,14 +1,14 @@
import log from 'loglevel'; import log from 'loglevel';
import React, {useRef} from 'react'; import React, { useRef } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import {NavLink} from "react-router-dom"; import { NavLink } from "react-router-dom";
import {flingClient} from '../../util/flingclient'; import { flingClient } from '../../util/flingclient';
function TileAction(props) { function TileAction(props) {
let shareUrlRef = useRef(null); let shareUrlRef = useRef(null);
return( return (
<div className="tile-action dropdown"> <div className="tile-action dropdown">
<button className="btn btn-link btn dropdown-toggle" tabIndex="0"> <button className="btn btn-link btn dropdown-toggle" tabIndex="0">
<i className="icon icon-more-vert" /> <i className="icon icon-more-vert" />
@ -16,21 +16,27 @@ function TileAction(props) {
<ul className="menu text-left"> <ul className="menu text-left">
<li className="menu-item input-group"> <li className="menu-item input-group">
<div className="input-group"> <div className="input-group">
<input type="text" ref={shareUrlRef} className="form-input input-sm input-share-id" readOnly value={props.fling.shareId} /> <input type="text" ref={shareUrlRef}
<span className="input-group-addon addon-sm input-group-addon-sm" onClick={copyShareUrl} ><i className="icon icon-copy" /></span> className="form-input input-sm input-share-id" readOnly
value={props.fling.shareId} />
<span className="input-group-addon addon-sm input-group-addon-sm"
onClick={copyShareUrl}>
<i className="icon icon-copy" /></span>
</div> </div>
</li> </li>
<li className="menu-item"> <li className="menu-item">
<div className="form-group"> <div className="form-group">
<label className="form-switch"> <label className="form-switch">
<input type="checkbox" checked={props.fling.shared} onChange={toggleShared} /> <input type="checkbox"
checked={props.fling.shared} onChange={toggleShared} />
<i className="form-icon" /> <i className="form-icon" />
{props.fling.shared ? "Shared":"Private"} {props.fling.shared ? "Shared" : "Private"}
</label> </label>
</div> </div>
</li> </li>
<li className="menu-item"> <li className="menu-item">
<button className="btn btn-link text-warning pl-0" onClick={deleteFling}> <button className="btn btn-link text-warning pl-0"
onClick={deleteFling}>
<i className="icon icon-delete mr-1" /> Remove <i className="icon icon-delete mr-1" /> Remove
</button> </button>
</li> </li>
@ -57,7 +63,7 @@ function TileAction(props) {
} }
async function toggleShared() { async function toggleShared() {
await flingClient.putFling(props.fling.id, {"sharing": {"shared": !props.fling.shared}}); await flingClient.putFling(props.fling.id, { "sharing": { "shared": !props.fling.shared } });
await props.refreshFlingListFn(); await props.refreshFlingListFn();
} }
} }
@ -65,7 +71,7 @@ function TileAction(props) {
export default function FlingTile(props) { export default function FlingTile(props) {
let tileClasses = classNames( let tileClasses = classNames(
"tile", "tile-centered", "p-2", "c-hand", "tile", "tile-centered", "p-2", "c-hand",
{"active": props.activeFling === props.fling.id} { "active": props.activeFling === props.fling.id }
); );
return ( return (
@ -74,7 +80,9 @@ export default function FlingTile(props) {
<div className="tile-content"> <div className="tile-content">
<NavLink to={`/admin/${props.fling.id}`}> <NavLink to={`/admin/${props.fling.id}`}>
<div className="tile-title">{props.fling.name}</div> <div className="tile-title">{props.fling.name}</div>
<small className="tile-subtitle text-gray">14MB · Public · 1 Jan, 2017</small> <small className="tile-subtitle text-gray">
14MB · Public · 1 Jan, 2017
</small>
</NavLink> </NavLink>
</div> </div>
<TileAction fling={props.fling} refreshFlingListFn={props.refreshFlingListFn} /> <TileAction fling={props.fling} refreshFlingListFn={props.refreshFlingListFn} />

View file

@ -6,8 +6,9 @@ import ReactDOM from 'react-dom';
import log from 'loglevel'; import log from 'loglevel';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { createStore } from 'redux'; import { createStore, applyMiddleware, compose } from 'redux';
import reducer from './redux/reducer'; import thunk from 'redux-thunk';
import rootReducer from './redux/reducers';
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
@ -19,14 +20,16 @@ import * as serviceWorker from './serviceWorker';
/* Logging Setup */ /* Logging Setup */
log.setDefaultLevel(log.levels.TRACE); log.setDefaultLevel(log.levels.TRACE);
if(process.env.REACT_APP_LOGLEVEL) { if (process.env.REACT_APP_LOGLEVEL) {
log.setLevel(process.env.REACT_APP_LOGLEVEL); log.setLevel(process.env.REACT_APP_LOGLEVEL);
} }
/* Store setup */ /* Store setup */
let store = createStore(reducer, let store = createStore(rootReducer,
compose(
applyMiddleware(thunk),
(window.__REDUX_DEVTOOLS_EXTENSION__ (window.__REDUX_DEVTOOLS_EXTENSION__
&& window.__REDUX_DEVTOOLS_EXTENSION__())); && window.__REDUX_DEVTOOLS_EXTENSION__())));
/* Fling App Setup */ /* Fling App Setup */
ReactDOM.render( ReactDOM.render(

View file

@ -0,0 +1,3 @@
export const SET_FLINGS = "SET_FLINGS";
export const ADD_FLING = "ADD_FLING";
export const SET_ACTIVE_FLING = "SET_ACTIVE_FLING";

View file

@ -0,0 +1,67 @@
import log from 'loglevel';
import { SET_FLINGS, SET_ACTIVE_FLING, ADD_FLING } from "./actionTypes";
import { FlingClient } from "../util/fc";
function setFlingsAction(flings) {
return {
type: SET_FLINGS,
payload: flings
}
}
function addFlingAction(fling) {
return {
type: ADD_FLING,
payload: fling
}
}
function setActiveFlingAction(fling) {
return {
type: SET_ACTIVE_FLING,
payload: fling
}
}
function setActiveFling(id) {
return (dispatch, getState) => {
if (!id) {
log.debug(`Not setting active Fling. No id given.`);
return;
}
const { flings: { flings } } = getState();
let foundFling = flings.find(f => f.id === id);
if (foundFling) {
log.info(`Found active fling ${id} in local storage`);
dispatch(setActiveFlingAction(foundFling));
} else {
log.info(`Active fling ${id} not found in local storage. ` +
`Trying to retrieve from remote.`);
let flingClient = new FlingClient();
flingClient.getFling(id)
.then(fling => {
dispatch(addFlingAction(fling));
dispatch(setActiveFlingAction(fling))
})
.catch(error => {
log.warn(`Could not find active fling. ` +
`Resetting active fling`);
dispatch(setActiveFlingAction(undefined));
})
}
}
}
function retrieveFlings() {
return (dispatch) => {
let flingClient = new FlingClient();
flingClient.getFlings()
.then(flings => dispatch(setFlingsAction(flings)));
}
}
export { retrieveFlings, setActiveFling };

View file

@ -1,3 +0,0 @@
export default function (state = {}, action) {
return;
};

View file

@ -0,0 +1,29 @@
import produce from "immer";
import { SET_FLINGS, SET_ACTIVE_FLING, ADD_FLING } from "../actionTypes";
const initialState = {
// type [fc.Fling]
flings: [],
// fc.Fling.id of the currently active fling
// or null of no fling is active
activeFling: null
}
export default produce((draft, action) => {
switch (action.type) {
case SET_FLINGS:
draft.flings = action.payload;
break;
case ADD_FLING:
draft.flings.push(action.payload);
break;
case SET_ACTIVE_FLING:
draft.activeFling = action.payload;
break;
default:
break;
}
return draft;
}, initialState);

View file

@ -0,0 +1,4 @@
import { combineReducers } from "redux";
import flings from "./flings";
export default combineReducers({ flings });

View file

@ -0,0 +1,3 @@
export const FLING_FILTERS = {
ALL: "all"
}

View file

View file

@ -0,0 +1,10 @@
import { FLING_FILTERS } from "../selectorTypes";
export const flingSelector = (store, flingFilter) => {
switch(flingFilter) {
case FLING_FILTERS.ALL:
return store.flings.flings;
default:
return [];
}
}

View file

@ -1,4 +0,0 @@
export default {
flings: [],
currentFling: undefined,
};

View file

@ -1,5 +1,12 @@
/*
* Shim for the fling API which sets a bearer token for every request
*/
import * as fc from '@fling/flingclient'; import * as fc from '@fling/flingclient';
/*
* Construct a client configuration with either the given token, or, if token is
* undefined or null, token retrieved from the session storage.
*/
let clientConfig = (token) => { let clientConfig = (token) => {
let config = new fc.ApiClient(); let config = new fc.ApiClient();
config.basePath = process.env.REACT_APP_API.replace(/\/+$/, ''); config.basePath = process.env.REACT_APP_API.replace(/\/+$/, '');