added playlist counting
This commit is contained in:
parent
849c0a5fa6
commit
b54ef10541
@ -91,7 +91,7 @@ def lastfm_username_required(func):
|
||||
user_dict = database.get_user_doc_ref(kwargs.get('username')).get().to_dict()
|
||||
|
||||
if user_dict:
|
||||
if user_dict.get('lastfm_username'):
|
||||
if user_dict.get('lastfm_username') and len(user_dict.get('lastfm_username')) > 0:
|
||||
return func(*args, **kwargs)
|
||||
else:
|
||||
logger.warning(f'no last.fm username for {user_dict["username"]}')
|
||||
|
@ -1,15 +1,23 @@
|
||||
from flask import Blueprint, jsonify, request
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
|
||||
from music.api.decorators import login_or_basic_auth, lastfm_username_required, spotify_link_required
|
||||
from music.api.decorators import login_or_basic_auth, lastfm_username_required, spotify_link_required, cloud_task
|
||||
import music.db.database as database
|
||||
from music.tasks.refresh_lastfm_stats import refresh_lastfm_stats
|
||||
|
||||
from spotfm.maths.counter import Counter
|
||||
from spotframework.model.uri import Uri
|
||||
|
||||
from google.cloud import tasks_v2
|
||||
|
||||
blueprint = Blueprint('spotfm-api', __name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
tasker = tasks_v2.CloudTasksClient()
|
||||
task_path = tasker.queue_path('sarsooxyz', 'europe-west2', 'spotify-executions')
|
||||
|
||||
|
||||
@blueprint.route('/count', methods=['GET'])
|
||||
@login_or_basic_auth
|
||||
@ -55,3 +63,56 @@ def count(username=None):
|
||||
}), 200
|
||||
else:
|
||||
return jsonify({'error': f'playlist {playlist_name} not found'}), 404
|
||||
|
||||
|
||||
@blueprint.route('/playlist/refresh', methods=['GET'])
|
||||
@login_or_basic_auth
|
||||
@spotify_link_required
|
||||
@lastfm_username_required
|
||||
def playlist_refresh(username=None):
|
||||
|
||||
playlist_name = request.args.get('name', None)
|
||||
|
||||
if playlist_name:
|
||||
|
||||
if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD':
|
||||
create_refresh_playlist_task(username, playlist_name)
|
||||
else:
|
||||
refresh_lastfm_stats(username, playlist_name)
|
||||
|
||||
return jsonify({'message': 'execution requested', 'status': 'success'}), 200
|
||||
|
||||
else:
|
||||
logger.warning('no playlist requested')
|
||||
return jsonify({"error": 'no name requested'}), 400
|
||||
|
||||
|
||||
@blueprint.route('/playlist/refresh/task', methods=['POST'])
|
||||
@cloud_task
|
||||
def run_playlist_task():
|
||||
|
||||
payload = request.get_data(as_text=True)
|
||||
if payload:
|
||||
payload = json.loads(payload)
|
||||
|
||||
logger.info(f'running {payload["username"]} / {payload["name"]}')
|
||||
|
||||
refresh_lastfm_stats(payload['username'], payload['name'])
|
||||
|
||||
return jsonify({'message': 'executed playlist', 'status': 'success'}), 200
|
||||
|
||||
|
||||
def create_refresh_playlist_task(username, playlist_name):
|
||||
|
||||
task = {
|
||||
'app_engine_http_request': { # Specify the type of request.
|
||||
'http_method': 'POST',
|
||||
'relative_uri': '/api/spotfm/playlist/refresh/task',
|
||||
'body': json.dumps({
|
||||
'username': username,
|
||||
'name': playlist_name
|
||||
}).encode()
|
||||
}
|
||||
}
|
||||
|
||||
tasker.create_task(task_path, task)
|
||||
|
@ -123,7 +123,7 @@ def get_user_playlists_collection(user_id: str) -> firestore.CollectionReference
|
||||
return playlists
|
||||
|
||||
|
||||
def get_user_playlist_ref_by_username(user: str, playlist: str) -> Optional[firestore.CollectionReference]:
|
||||
def get_user_playlist_ref_by_username(user: str, playlist: str) -> Optional[firestore.DocumentReference]:
|
||||
|
||||
user_ref = get_user_doc_ref(user)
|
||||
|
||||
@ -137,7 +137,7 @@ def get_user_playlist_ref_by_username(user: str, playlist: str) -> Optional[fire
|
||||
|
||||
|
||||
def get_user_playlist_ref_by_user_ref(user_ref: firestore.DocumentReference,
|
||||
playlist: str) -> Optional[firestore.CollectionReference]:
|
||||
playlist: str) -> Optional[firestore.DocumentReference]:
|
||||
|
||||
playlist_collection = get_user_playlists_collection(user_ref.id)
|
||||
|
||||
|
36
music/tasks/refresh_lastfm_stats.py
Normal file
36
music/tasks/refresh_lastfm_stats.py
Normal file
@ -0,0 +1,36 @@
|
||||
from google.cloud import firestore
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import music.db.database as database
|
||||
|
||||
from spotfm.maths.counter import Counter
|
||||
from spotframework.model.uri import Uri
|
||||
|
||||
db = firestore.Client()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def refresh_lastfm_stats(username, playlist_name):
|
||||
|
||||
logger.info(f'refreshing {playlist_name} stats for {username}')
|
||||
|
||||
fmnet = database.get_authed_lastfm_network(username=username)
|
||||
spotnet = database.get_authed_spotify_network(username=username)
|
||||
counter = Counter(fmnet=fmnet, spotnet=spotnet)
|
||||
|
||||
database_ref = database.get_user_playlist_ref_by_username(user=username, playlist=playlist_name)
|
||||
|
||||
playlist_dict = database_ref.get().to_dict()
|
||||
count = counter.count(Uri(playlist_dict['uri']))
|
||||
|
||||
user_count = fmnet.get_user_scrobble_count()
|
||||
percent = round((count * 100) / user_count, 2)
|
||||
|
||||
database_ref.update({
|
||||
'lastfm_stat_count': count,
|
||||
'lastfm_stat_percent': percent,
|
||||
'lastfm_stat_last_refresh': datetime.utcnow()
|
||||
})
|
@ -146,7 +146,7 @@ function PlaylistLink(props){
|
||||
}
|
||||
|
||||
function getPlaylistLink(playlistName){
|
||||
return '/app/playlist/' + playlistName;
|
||||
return `/app/playlist/${playlistName}/edit`;
|
||||
}
|
||||
|
||||
export default PlaylistsView;
|
79
src/js/Playlist/View/Count.js
Normal file
79
src/js/Playlist/View/Count.js
Normal file
@ -0,0 +1,79 @@
|
||||
import React, { Component } from "react";
|
||||
const axios = require('axios');
|
||||
|
||||
import showMessage from "../../Toast.js"
|
||||
|
||||
class Count extends Component {
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
name: props.name,
|
||||
lastfm_refresh: 'never',
|
||||
lastfm_percent: 0,
|
||||
count: 0,
|
||||
isLoading: true
|
||||
}
|
||||
this.getUserInfo();
|
||||
|
||||
this.updateStats = this.updateStats.bind(this);
|
||||
}
|
||||
|
||||
getUserInfo(){
|
||||
axios.get(`/api/playlist?name=${ this.state.name }`)
|
||||
.then((response) => {
|
||||
if(response.data.lastfm_stat_last_refresh != undefined){
|
||||
this.setState({
|
||||
count: response.data.lastfm_stat_count,
|
||||
lastfm_refresh: response.data.lastfm_stat_last_refresh,
|
||||
lastfm_percent: response.data.lastfm_stat_percent,
|
||||
isLoading: false
|
||||
})
|
||||
}else{
|
||||
showMessage('no stats for this playlist');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
showMessage(`error getting playlist info (${error.response.status})`);
|
||||
});
|
||||
}
|
||||
|
||||
updateStats(){
|
||||
axios.get(`/api/spotfm/playlist/refresh?name=${ this.state.name }`)
|
||||
.then((response) => {
|
||||
showMessage('stats refresh queued');
|
||||
})
|
||||
.catch((error) => {
|
||||
if(error.response.status == 401){
|
||||
showMessage('missing either spotify or last.fm link');
|
||||
}else{
|
||||
showMessage(`error refreshing (${error.response.status})`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ui-text center-text text-no-select">scrobble count: <b>{this.state.count.toLocaleString()}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="ui-text center-text text-no-select">that's <b>{this.state.lastfm_percent}%</b> of all scrobbles</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="ui-text center-text text-no-select">last updated <b>{this.state.lastfm_refresh.toLocaleString()}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan="2">
|
||||
<button style={{width: "100%"}} className="button" onClick={this.updateStats}>update</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default Count;
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from "react";
|
||||
const axios = require('axios');
|
||||
|
||||
import showMessage from "../Toast.js"
|
||||
import showMessage from "../../Toast.js"
|
||||
|
||||
var thisMonth = [
|
||||
'january',
|
||||
@ -33,12 +33,12 @@ var lastMonth = [
|
||||
'november'
|
||||
];
|
||||
|
||||
class PlaylistView extends Component{
|
||||
class Edit extends Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
name: this.props.match.params.name,
|
||||
name: this.props.name,
|
||||
parts: [],
|
||||
playlists: [],
|
||||
filteredPlaylists: [],
|
||||
@ -352,162 +352,157 @@ class PlaylistView extends Component{
|
||||
var date = new Date();
|
||||
|
||||
const table = (
|
||||
<table className="app-table max-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan="2"><h1 className="text-no-select">{ this.state.name }</h1></th>
|
||||
</tr>
|
||||
</thead>
|
||||
{ this.state.playlist_references.length > 0 && <ListBlock name="managed" handler={this.handleRemoveReference} list={this.state.playlist_references}/> }
|
||||
{ this.state.parts.length > 0 && <ListBlock name="spotify" handler={this.handleRemovePart} list={this.state.parts}/> }
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan="2" className="center-text ui-text text-no-select" style={{fontStyle: "italic"}}>
|
||||
<br></br>spotify playlist can be the name of either your own created playlist or one you follow, names are case sensitive
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="text"
|
||||
name="newPlaylistName"
|
||||
<tbody>
|
||||
{ this.state.playlist_references.length > 0 && <tr><td colSpan="2" className="ui-text center-text text-no-select" style={{fontStyle: 'italic'}}>managed</td></tr> }
|
||||
{ this.state.playlist_references.length > 0 && <ListBlock handler={this.handleRemoveReference} list={this.state.playlist_references}/> }
|
||||
|
||||
{ this.state.parts.length > 0 && <tr><td colSpan="2" className="ui-text center-text text-no-select" style={{fontStyle: 'italic'}}>spotify</td></tr> }
|
||||
{ this.state.parts.length > 0 && <ListBlock handler={this.handleRemovePart} list={this.state.parts}/> }
|
||||
<tr>
|
||||
<td colSpan="2" className="center-text ui-text text-no-select" style={{fontStyle: "italic"}}>
|
||||
<br></br>spotify playlist can be the name of either your own created playlist or one you follow, names are case sensitive
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="text"
|
||||
name="newPlaylistName"
|
||||
className="full-width"
|
||||
value={this.state.newPlaylistName}
|
||||
onChange={this.handleInputChange}
|
||||
placeholder="spotify playlist name"></input>
|
||||
</td>
|
||||
<td>
|
||||
<button className="button full-width" onClick={this.handleAddPart}>add</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<select name="newReferenceName"
|
||||
className="full-width"
|
||||
value={this.state.newPlaylistName}
|
||||
onChange={this.handleInputChange}
|
||||
placeholder="spotify playlist name"></input>
|
||||
</td>
|
||||
<td>
|
||||
<button className="button full-width" onClick={this.handleAddPart}>add</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<select name="newReferenceName"
|
||||
className="full-width"
|
||||
value={this.state.newReferenceName}
|
||||
onChange={this.handleInputChange}>
|
||||
{ this.state.playlists
|
||||
.filter((entry) => entry.name != this.state.name)
|
||||
.map((entry) => <ReferenceEntry name={entry.name} key={entry.name} />) }
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button className="button full-width" onClick={this.handleAddReference}>add</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
shuffle output?
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
checked={this.state.shuffle}
|
||||
onChange={this.handleShuffleChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
include recommendations?
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
checked={this.state.include_recommendations}
|
||||
onChange={this.handleIncludeRecommendationsChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
number of recommendations
|
||||
</td>
|
||||
<td>
|
||||
value={this.state.newReferenceName}
|
||||
onChange={this.handleInputChange}>
|
||||
{ this.state.playlists
|
||||
.filter((entry) => entry.name != this.state.name)
|
||||
.map((entry) => <ReferenceEntry name={entry.name} key={entry.name} />) }
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button className="button full-width" onClick={this.handleAddReference}>add</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
shuffle output?
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
checked={this.state.shuffle}
|
||||
onChange={this.handleShuffleChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
include recommendations?
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
checked={this.state.include_recommendations}
|
||||
onChange={this.handleIncludeRecommendationsChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
number of recommendations
|
||||
</td>
|
||||
<td>
|
||||
<input type="number"
|
||||
name="recommendation_sample"
|
||||
className="full-width"
|
||||
value={this.state.recommendation_sample}
|
||||
onChange={this.handleInputChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
{ this.state.type == 'recents' &&
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
added since (days)
|
||||
</td>
|
||||
<td>
|
||||
<input type="number"
|
||||
name="recommendation_sample"
|
||||
className="full-width"
|
||||
value={this.state.recommendation_sample}
|
||||
onChange={this.handleInputChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
{ this.state.type == 'recents' &&
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
added since (days)
|
||||
</td>
|
||||
<td>
|
||||
<input type="number"
|
||||
name="day_boundary"
|
||||
className="full-width"
|
||||
value={this.state.day_boundary}
|
||||
onChange={this.handleInputChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
{ this.state.type == 'recents' &&
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
include {thisMonth[date.getMonth()]} playlist
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
checked={this.state.add_this_month}
|
||||
onChange={this.handleThisMonthChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
{ this.state.type == 'recents' &&
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
include {lastMonth[date.getMonth()]} playlist
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
checked={this.state.add_last_month}
|
||||
onChange={this.handleLastMonthChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
playlist type
|
||||
</td>
|
||||
<td>
|
||||
<select className="full-width"
|
||||
name="type"
|
||||
onChange={this.handleInputChange}
|
||||
value={this.state.type}>
|
||||
<option value="default">default</option>
|
||||
<option value="recents">recents</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan="2">
|
||||
<button className="button full-width" onClick={this.handleRun}>run</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
name="day_boundary"
|
||||
className="full-width"
|
||||
value={this.state.day_boundary}
|
||||
onChange={this.handleInputChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
{ this.state.type == 'recents' &&
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
include {thisMonth[date.getMonth()]} playlist
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
checked={this.state.add_this_month}
|
||||
onChange={this.handleThisMonthChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
{ this.state.type == 'recents' &&
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
include {lastMonth[date.getMonth()]} playlist
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
checked={this.state.add_last_month}
|
||||
onChange={this.handleLastMonthChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
playlist type
|
||||
</td>
|
||||
<td>
|
||||
<select className="full-width"
|
||||
name="type"
|
||||
onChange={this.handleInputChange}
|
||||
value={this.state.type}>
|
||||
<option value="default">default</option>
|
||||
<option value="recents">recents</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan="2">
|
||||
<button className="button full-width" onClick={this.handleRun}>run</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
);
|
||||
|
||||
const error = <p style={{textAlign: "center"}}>{ this.state.error_text }</p>;
|
||||
const loadingMessage = <p className="center-text">loading...</p>;
|
||||
const loadingMessage =
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<p className="center-text">loading...</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>;
|
||||
|
||||
return this.state.isLoading ? loadingMessage : ( this.state.error ? error : table );
|
||||
return this.state.isLoading ? loadingMessage : table;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function ReferenceEntry(props) {
|
||||
return (
|
||||
<option value={props.name}>{props.name}</option>
|
||||
);
|
||||
return <option value={props.name}>{props.name}</option>;
|
||||
}
|
||||
|
||||
function ListBlock(props) {
|
||||
return (
|
||||
<tbody>
|
||||
<tr><td colSpan="2" className="ui-text center-text text-no-select" style={{fontStyle: 'italic'}}>{props.name}</td></tr>
|
||||
{ props.list.map((part) => <Row part={ part } key={ part } handler={props.handler}/>) }
|
||||
</tbody>
|
||||
);
|
||||
return props.list.map((part) => <Row part={ part } key={ part } handler={props.handler}/>);
|
||||
}
|
||||
|
||||
function Row (props) {
|
||||
@ -519,4 +514,4 @@ function Row (props) {
|
||||
);
|
||||
}
|
||||
|
||||
export default PlaylistView;
|
||||
export default Edit;
|
36
src/js/Playlist/View/View.js
Normal file
36
src/js/Playlist/View/View.js
Normal file
@ -0,0 +1,36 @@
|
||||
import React, { Component } from "react";
|
||||
import { BrowserRouter as Router, Route, Link, Switch, Redirect} from "react-router-dom";
|
||||
const axios = require('axios');
|
||||
|
||||
import Edit from "./Edit.js";
|
||||
import Count from "./Count.js";
|
||||
|
||||
class View extends Component{
|
||||
|
||||
render() {
|
||||
return (
|
||||
<table className="app-table max-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan="2"><h1 className="text-no-select">{ this.props.match.params.name }</h1></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colSpan="2">
|
||||
<div>
|
||||
<ul className="navbar" style={{width: "100%"}}>
|
||||
<li><Link to={`${this.props.match.url}/edit`}>edit</Link></li>
|
||||
<li><Link to={`${this.props.match.url}/count`}>count</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<Route path={`${this.props.match.url}/edit`} render={(props) => <Edit {...props} name={this.props.match.params.name}/>} />
|
||||
<Route path={`${this.props.match.url}/count`} render={(props) => <Count {...props} name={this.props.match.params.name}/>} />
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default View;
|
@ -4,7 +4,7 @@ import { BrowserRouter as Router, Route, Link, Switch, Redirect} from "react-rou
|
||||
import Index from "./Index/Index.js";
|
||||
import Maths from "./Maths/Maths.js";
|
||||
import Playlists from "./Playlist/Playlists.js";
|
||||
import PlaylistView from "./Playlist/PlaylistView.js";
|
||||
import PlaylistView from "./Playlist/View/View.js";
|
||||
import Settings from "./Settings/Settings.js";
|
||||
import Admin from "./Admin/Admin.js";
|
||||
|
||||
@ -58,7 +58,7 @@ class PlaylistManager extends Component {
|
||||
<tbody>
|
||||
<tr><td><span><Link to="/app">home</Link></span></td></tr>
|
||||
<tr><td><Link to="/app/playlists">playlists</Link></td></tr>
|
||||
<tr><td><Link to="/app/maths">maths</Link></td></tr>
|
||||
{/* <tr><td><Link to="/app/maths">maths</Link></td></tr> */}
|
||||
<tr><td><Link to="/app/settings/password">settings</Link></td></tr>
|
||||
{ this.state.type == 'admin' && <tr><td><Link to="/app/admin/lock">admin</Link></td></tr> }
|
||||
<tr><td><a href="/auth/logout">logout</a></td></tr>
|
||||
|
Loading…
Reference in New Issue
Block a user