From 41f2a22f3d48c065629c6856b45ee80384526ef4 Mon Sep 17 00:00:00 2001 From: Armin Friedl Date: Sun, 19 Jul 2020 16:12:59 +0200 Subject: [PATCH] Introduce redux store --- examples/python/fling.py | 32 ++++ examples/querysheet.http | 39 +++++ web/fling/package-lock.json | 16 +- web/fling/package.json | 2 + web/fling/src/App.jsx | 37 ++++- web/fling/src/components/LandingPage.jsx | 1 - web/fling/src/components/admin/Error.jsx | 23 --- web/fling/src/components/admin/FlingAdmin.jsx | 43 ++++-- web/fling/src/components/admin/FlingList.jsx | 27 +--- web/fling/src/components/admin/FlingTile.jsx | 140 +++++++++--------- web/fling/src/index.js | 31 ++-- web/fling/src/redux/actionTypes.js | 3 + web/fling/src/redux/actions.js | 67 +++++++++ web/fling/src/redux/reducer.js | 3 - web/fling/src/redux/reducers/flings.js | 29 ++++ web/fling/src/redux/reducers/index.js | 4 + web/fling/src/redux/selectorTypes.js | 3 + web/fling/src/redux/selectors.js | 0 .../src/redux/selectors/flingSelectors.js | 10 ++ web/fling/src/redux/state.js | 4 - web/fling/src/util/fc.js | 7 + 21 files changed, 365 insertions(+), 156 deletions(-) create mode 100644 examples/python/fling.py create mode 100644 examples/querysheet.http delete mode 100644 web/fling/src/components/admin/Error.jsx create mode 100644 web/fling/src/redux/actionTypes.js delete mode 100644 web/fling/src/redux/reducer.js create mode 100644 web/fling/src/redux/reducers/flings.js create mode 100644 web/fling/src/redux/reducers/index.js create mode 100644 web/fling/src/redux/selectorTypes.js create mode 100644 web/fling/src/redux/selectors.js create mode 100644 web/fling/src/redux/selectors/flingSelectors.js delete mode 100644 web/fling/src/redux/state.js diff --git a/examples/python/fling.py b/examples/python/fling.py new file mode 100644 index 0000000..dc0d2dd --- /dev/null +++ b/examples/python/fling.py @@ -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 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}") + + # diff --git a/examples/querysheet.http b/examples/querysheet.http new file mode 100644 index 0000000..a7b16a6 --- /dev/null +++ b/examples/querysheet.http @@ -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)))) diff --git a/web/fling/package-lock.json b/web/fling/package-lock.json index 554398d..0404fa6 100644 --- a/web/fling/package-lock.json +++ b/web/fling/package-lock.json @@ -6740,9 +6740,9 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" }, "immer": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz", - "integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==" + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.5.tgz", + "integrity": "sha512-TtRAKZyuqld2eYjvWgXISLJ0ZlOl1OOTzRmrmiY8SlB0dnAhZ1OiykIDL5KDFNaPHDXiLfGQFNJGtet8z8AEmg==" }, "import-cwd": { "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": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", @@ -11922,6 +11927,11 @@ "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": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", diff --git a/web/fling/package.json b/web/fling/package.json index 1d53417..4babb47 100644 --- a/web/fling/package.json +++ b/web/fling/package.json @@ -10,6 +10,7 @@ "axios": "^0.19.2", "classnames": "^2.2.6", "core-js": "^3.6.5", + "immer": "^7.0.5", "jwt-decode": "^2.2.0", "loglevel": "^1.6.8", "node-sass": "^4.14.1", @@ -20,6 +21,7 @@ "react-router-dom": "^5.1.2", "react-scripts": "3.4.1", "redux": "^4.0.5", + "redux-thunk": "^2.3.0", "spectre.css": "^0.5.8" }, "scripts": { diff --git a/web/fling/src/App.jsx b/web/fling/src/App.jsx index 5cfcb31..4c9f970 100644 --- a/web/fling/src/App.jsx +++ b/web/fling/src/App.jsx @@ -1,7 +1,7 @@ import log from 'loglevel'; 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'; @@ -12,6 +12,19 @@ import Unlock from './components/user/Unlock'; import FlingUser from './components/user/FlingUser'; 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 () => { return ( @@ -19,7 +32,7 @@ export default () => { - + @@ -45,14 +58,19 @@ function OwnerRoute({ children, ...rest }) { {...rest} render={({ location }) => { if (jwt.hasSubject("admin")) { return children; } - else { return ; } + else { + return ; + } }} /> ); } -/* A wrapper for that redirects to the unlock screen if no authorized token - * was found. +/* A wrapper for that redirects to the unlock screen if no authorized + * token * was found. * * 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 @@ -65,11 +83,14 @@ function UserRoute({ children, ...rest }) { { - let state = {from: location, shareId: match.params.shareId}; - let authorized = jwt.hasSubject("admin") || (jwt.hasSubject("user") && jwt.hasClaim("id", state['shareId'])); + let state = { from: location, shareId: match.params.shareId }; + + let authorized = + jwt.hasSubject("admin") + || ( jwt.hasSubject("user") && jwt.hasClaim("id", state['shareId']) ); if (authorized) { return children; } - else { return ; } + else { return ; } }} /> ); diff --git a/web/fling/src/components/LandingPage.jsx b/web/fling/src/components/LandingPage.jsx index 1d48787..ac5995d 100644 --- a/web/fling/src/components/LandingPage.jsx +++ b/web/fling/src/components/LandingPage.jsx @@ -13,7 +13,6 @@ export default function LandingPage() { function openFling(ev) { ev.preventDefault(); - window.location = `/f/${shareId}`; } diff --git a/web/fling/src/components/admin/Error.jsx b/web/fling/src/components/admin/Error.jsx deleted file mode 100644 index 25e07b9..0000000 --- a/web/fling/src/components/admin/Error.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - -export default (props) => { - function renderError() { - return ( -
- -
Ooops!
-
  • - { props.errors.map( (err, idx) =>
      {err}
    ) } -
  • -
    - ); - } - - return ( - <> - { props.errors.length > 0 && !props.below ? renderError() : "" } - { props.children } - { props.errors.length > 0 && props.below ? renderError() : "" } - - ); -} diff --git a/web/fling/src/components/admin/FlingAdmin.jsx b/web/fling/src/components/admin/FlingAdmin.jsx index 50ce0ca..41d6784 100644 --- a/web/fling/src/components/admin/FlingAdmin.jsx +++ b/web/fling/src/components/admin/FlingAdmin.jsx @@ -1,24 +1,41 @@ -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 FlingList from './FlingList'; import FlingContent from './FlingContent'; -import {useParams} from 'react-router-dom'; - 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 ( +
    + + +
    +
    +
    + +
    +
    +
    - ); +
    +
    + ); } diff --git a/web/fling/src/components/admin/FlingList.jsx b/web/fling/src/components/admin/FlingList.jsx index 99da35d..6a6f1a8 100644 --- a/web/fling/src/components/admin/FlingList.jsx +++ b/web/fling/src/components/admin/FlingList.jsx @@ -1,33 +1,18 @@ -import log from 'loglevel'; -import React, {useState, useEffect} from 'react'; - -import {FlingClient} from '../../util/fc'; +import React from 'react'; +import { useSelector } from "react-redux"; import FlingTile from './FlingTile'; -export default function FlingList(props) { - const [flings, setFlings] = useState([]); - useEffect(() => { - let flingClient = new FlingClient(); - flingClient.getFlings() - .then(flings => { - let newFlings = []; - for (let fling of flings) { - let flingTile = ; - newFlings.push(flingTile); - } - setFlings(newFlings); - }).catch(log.error); - }, []); +export default function FlingList() { + const flings = useSelector((store) => store.flings.flings); - return( + return (
    - {log.info(`Got active fling: ${props.activeFling}`)}
    My Flings
    - {flings} + {flings.map(fling => )}
    ); diff --git a/web/fling/src/components/admin/FlingTile.jsx b/web/fling/src/components/admin/FlingTile.jsx index fd5f00f..c46f82a 100644 --- a/web/fling/src/components/admin/FlingTile.jsx +++ b/web/fling/src/components/admin/FlingTile.jsx @@ -1,84 +1,92 @@ import log from 'loglevel'; -import React, {useRef} from 'react'; +import React, { useRef } from 'react'; 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) { - let shareUrlRef = useRef(null); + let shareUrlRef = useRef(null); - return( -
    - +
      +
    • +
      + + + +
      +
    • +
    • +
      + +
      +
    • +
    • + -
        -
      • -
        - - -
        -
      • -
      • -
        - -
        -
      • -
      • - -
      • -
      -
    - ); + + +
    + ); - function copyShareUrl() { - shareUrlRef.current.focus(); - shareUrlRef.current.select(); + function copyShareUrl() { + shareUrlRef.current.focus(); + shareUrlRef.current.select(); - try { - let successful = document.execCommand('copy'); - let msg = successful ? 'successful' : 'unsuccessful'; - console.log('Copying to clipoard ' + msg); - } catch (err) { - log.error("Couldn't copy to clipboard: ", err); - } + try { + let successful = document.execCommand('copy'); + let msg = successful ? 'successful' : 'unsuccessful'; + console.log('Copying to clipoard ' + msg); + } catch (err) { + log.error("Couldn't copy to clipboard: ", err); } + } - async function deleteFling() { - await flingClient.deleteFling(props.fling.id); - await props.refreshFlingListFn(); - } + async function deleteFling() { + await flingClient.deleteFling(props.fling.id); + await props.refreshFlingListFn(); + } - async function toggleShared() { - await flingClient.putFling(props.fling.id, {"sharing": {"shared": !props.fling.shared}}); - await props.refreshFlingListFn(); - } + async function toggleShared() { + await flingClient.putFling(props.fling.id, { "sharing": { "shared": !props.fling.shared } }); + await props.refreshFlingListFn(); + } } export default function FlingTile(props) { - let tileClasses = classNames( - "tile", "tile-centered", "p-2", "c-hand", - {"active": props.activeFling === props.fling.id} - ); + let tileClasses = classNames( + "tile", "tile-centered", "p-2", "c-hand", + { "active": props.activeFling === props.fling.id } + ); - return ( -
    -
    -
    - -
    {props.fling.name}
    - 14MB · Public · 1 Jan, 2017 -
    -
    - -
    + return ( +
    +
    +
    + +
    {props.fling.name}
    + + 14MB · Public · 1 Jan, 2017 + +
    - ); + +
    +
    + ); } diff --git a/web/fling/src/index.js b/web/fling/src/index.js index 1b67669..1172ef8 100644 --- a/web/fling/src/index.js +++ b/web/fling/src/index.js @@ -6,8 +6,9 @@ import ReactDOM from 'react-dom'; import log from 'loglevel'; import { Provider } from 'react-redux'; -import { createStore } from 'redux'; -import reducer from './redux/reducer'; +import { createStore, applyMiddleware, compose } from 'redux'; +import thunk from 'redux-thunk'; +import rootReducer from './redux/reducers'; import { BrowserRouter } from "react-router-dom"; @@ -19,24 +20,26 @@ import * as serviceWorker from './serviceWorker'; /* Logging Setup */ log.setDefaultLevel(log.levels.TRACE); -if(process.env.REACT_APP_LOGLEVEL) { - log.setLevel(process.env.REACT_APP_LOGLEVEL); +if (process.env.REACT_APP_LOGLEVEL) { + log.setLevel(process.env.REACT_APP_LOGLEVEL); } /* Store setup */ -let store = createStore(reducer, - (window.__REDUX_DEVTOOLS_EXTENSION__ - && window.__REDUX_DEVTOOLS_EXTENSION__())); +let store = createStore(rootReducer, + compose( + applyMiddleware(thunk), + (window.__REDUX_DEVTOOLS_EXTENSION__ + && window.__REDUX_DEVTOOLS_EXTENSION__()))); /* Fling App Setup */ ReactDOM.render( - - - - - - - , + + + + + + + , document.getElementById('root') ); diff --git a/web/fling/src/redux/actionTypes.js b/web/fling/src/redux/actionTypes.js new file mode 100644 index 0000000..ab31344 --- /dev/null +++ b/web/fling/src/redux/actionTypes.js @@ -0,0 +1,3 @@ +export const SET_FLINGS = "SET_FLINGS"; +export const ADD_FLING = "ADD_FLING"; +export const SET_ACTIVE_FLING = "SET_ACTIVE_FLING"; diff --git a/web/fling/src/redux/actions.js b/web/fling/src/redux/actions.js index e69de29..27a09fe 100644 --- a/web/fling/src/redux/actions.js +++ b/web/fling/src/redux/actions.js @@ -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 }; diff --git a/web/fling/src/redux/reducer.js b/web/fling/src/redux/reducer.js deleted file mode 100644 index 1e8d47f..0000000 --- a/web/fling/src/redux/reducer.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function (state = {}, action) { - return; -}; diff --git a/web/fling/src/redux/reducers/flings.js b/web/fling/src/redux/reducers/flings.js new file mode 100644 index 0000000..f5d1f08 --- /dev/null +++ b/web/fling/src/redux/reducers/flings.js @@ -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); diff --git a/web/fling/src/redux/reducers/index.js b/web/fling/src/redux/reducers/index.js new file mode 100644 index 0000000..d4756e5 --- /dev/null +++ b/web/fling/src/redux/reducers/index.js @@ -0,0 +1,4 @@ +import { combineReducers } from "redux"; +import flings from "./flings"; + +export default combineReducers({ flings }); diff --git a/web/fling/src/redux/selectorTypes.js b/web/fling/src/redux/selectorTypes.js new file mode 100644 index 0000000..0d0fda6 --- /dev/null +++ b/web/fling/src/redux/selectorTypes.js @@ -0,0 +1,3 @@ +export const FLING_FILTERS = { + ALL: "all" +} diff --git a/web/fling/src/redux/selectors.js b/web/fling/src/redux/selectors.js new file mode 100644 index 0000000..e69de29 diff --git a/web/fling/src/redux/selectors/flingSelectors.js b/web/fling/src/redux/selectors/flingSelectors.js new file mode 100644 index 0000000..a6126f2 --- /dev/null +++ b/web/fling/src/redux/selectors/flingSelectors.js @@ -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 []; + } +} diff --git a/web/fling/src/redux/state.js b/web/fling/src/redux/state.js deleted file mode 100644 index 4e6c41c..0000000 --- a/web/fling/src/redux/state.js +++ /dev/null @@ -1,4 +0,0 @@ -export default { - flings: [], - currentFling: undefined, -}; diff --git a/web/fling/src/util/fc.js b/web/fling/src/util/fc.js index 11445d7..905186a 100644 --- a/web/fling/src/util/fc.js +++ b/web/fling/src/util/fc.js @@ -1,5 +1,12 @@ +/* + * Shim for the fling API which sets a bearer token for every request + */ 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 config = new fc.ApiClient(); config.basePath = process.env.REACT_APP_API.replace(/\/+$/, '');