This commit is contained in:
parent
0c1fe8efce
commit
41f2a22f3d
21 changed files with 365 additions and 156 deletions
32
examples/python/fling.py
Normal file
32
examples/python/fling.py
Normal 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
39
examples/querysheet.http
Normal 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))))
|
16
web/fling/package-lock.json
generated
16
web/fling/package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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 }} />; }
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() : "" }
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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 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(() => {
|
||||||
<div>
|
dispatch(retrieveFlings());
|
||||||
<Navbar />
|
}, [dispatch]);
|
||||||
|
|
||||||
<div className="container">
|
useEffect(() => {
|
||||||
<div className="columns mt-2">
|
if (flingId) {
|
||||||
<div className="column col-sm-12 col-lg-3 col-2"> <FlingList activeFling={fling} /> </div>
|
dispatch(setActiveFling(flingId))
|
||||||
<div className="column col-sm-12"><FlingContent activeFling={fling} /></div>
|
}
|
||||||
</div>
|
}, [flingId, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<div className="container">
|
||||||
|
<div className="columns mt-2">
|
||||||
|
<div className="column col-sm-12 col-lg-3 col-2">
|
||||||
|
<FlingList />
|
||||||
|
</div>
|
||||||
|
<div className="column col-sm-12">
|
||||||
|
<FlingContent />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,84 +1,92 @@
|
||||||
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" />
|
||||||
|
</button>
|
||||||
|
<ul className="menu text-left">
|
||||||
|
<li className="menu-item input-group">
|
||||||
|
<div className="input-group">
|
||||||
|
<input type="text" ref={shareUrlRef}
|
||||||
|
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>
|
||||||
|
</li>
|
||||||
|
<li className="menu-item">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-switch">
|
||||||
|
<input type="checkbox"
|
||||||
|
checked={props.fling.shared} onChange={toggleShared} />
|
||||||
|
<i className="form-icon" />
|
||||||
|
{props.fling.shared ? "Shared" : "Private"}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="menu-item">
|
||||||
|
<button className="btn btn-link text-warning pl-0"
|
||||||
|
onClick={deleteFling}>
|
||||||
|
<i className="icon icon-delete mr-1" /> Remove
|
||||||
</button>
|
</button>
|
||||||
<ul className="menu text-left">
|
</li>
|
||||||
<li className="menu-item input-group">
|
</ul>
|
||||||
<div className="input-group">
|
</div>
|
||||||
<input type="text" ref={shareUrlRef} 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>
|
|
||||||
</li>
|
|
||||||
<li className="menu-item">
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-switch">
|
|
||||||
<input type="checkbox" checked={props.fling.shared} onChange={toggleShared} />
|
|
||||||
<i className="form-icon" />
|
|
||||||
{props.fling.shared ? "Shared":"Private"}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li className="menu-item">
|
|
||||||
<button className="btn btn-link text-warning pl-0" onClick={deleteFling}>
|
|
||||||
<i className="icon icon-delete mr-1" /> Remove
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
function copyShareUrl() {
|
function copyShareUrl() {
|
||||||
shareUrlRef.current.focus();
|
shareUrlRef.current.focus();
|
||||||
shareUrlRef.current.select();
|
shareUrlRef.current.select();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let successful = document.execCommand('copy');
|
let successful = document.execCommand('copy');
|
||||||
let msg = successful ? 'successful' : 'unsuccessful';
|
let msg = successful ? 'successful' : 'unsuccessful';
|
||||||
console.log('Copying to clipoard ' + msg);
|
console.log('Copying to clipoard ' + msg);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error("Couldn't copy to clipboard: ", err);
|
log.error("Couldn't copy to clipboard: ", err);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteFling() {
|
async function deleteFling() {
|
||||||
await flingClient.deleteFling(props.fling.id);
|
await flingClient.deleteFling(props.fling.id);
|
||||||
await props.refreshFlingListFn();
|
await props.refreshFlingListFn();
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
||||||
<div>
|
<div>
|
||||||
<div className={tileClasses}>
|
<div className={tileClasses}>
|
||||||
<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">
|
||||||
</NavLink>
|
14MB · Public · 1 Jan, 2017
|
||||||
</div>
|
</small>
|
||||||
<TileAction fling={props.fling} refreshFlingListFn={props.refreshFlingListFn} />
|
</NavLink>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<TileAction fling={props.fling} refreshFlingListFn={props.refreshFlingListFn} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,24 +20,26 @@ 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,
|
||||||
(window.__REDUX_DEVTOOLS_EXTENSION__
|
compose(
|
||||||
&& window.__REDUX_DEVTOOLS_EXTENSION__()));
|
applyMiddleware(thunk),
|
||||||
|
(window.__REDUX_DEVTOOLS_EXTENSION__
|
||||||
|
&& window.__REDUX_DEVTOOLS_EXTENSION__())));
|
||||||
|
|
||||||
/* Fling App Setup */
|
/* Fling App Setup */
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
3
web/fling/src/redux/actionTypes.js
Normal file
3
web/fling/src/redux/actionTypes.js
Normal 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";
|
|
@ -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 };
|
|
@ -1,3 +0,0 @@
|
||||||
export default function (state = {}, action) {
|
|
||||||
return;
|
|
||||||
};
|
|
29
web/fling/src/redux/reducers/flings.js
Normal file
29
web/fling/src/redux/reducers/flings.js
Normal 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);
|
4
web/fling/src/redux/reducers/index.js
Normal file
4
web/fling/src/redux/reducers/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { combineReducers } from "redux";
|
||||||
|
import flings from "./flings";
|
||||||
|
|
||||||
|
export default combineReducers({ flings });
|
3
web/fling/src/redux/selectorTypes.js
Normal file
3
web/fling/src/redux/selectorTypes.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const FLING_FILTERS = {
|
||||||
|
ALL: "all"
|
||||||
|
}
|
0
web/fling/src/redux/selectors.js
Normal file
0
web/fling/src/redux/selectors.js
Normal file
10
web/fling/src/redux/selectors/flingSelectors.js
Normal file
10
web/fling/src/redux/selectors/flingSelectors.js
Normal 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 [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
export default {
|
|
||||||
flings: [],
|
|
||||||
currentFling: undefined,
|
|
||||||
};
|
|
|
@ -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(/\/+$/, '');
|
||||||
|
|
Loading…
Reference in a new issue