add model upload functionality; add launch tournament functionality; add model removal functionality

This commit is contained in:
Songyi Huang 2020-09-08 23:55:02 -07:00
parent 379edbb4f3
commit a41f8c5fd8
3 changed files with 216 additions and 15 deletions

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import axios from 'axios';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import Drawer from '@material-ui/core/Drawer'; import Drawer from '@material-ui/core/Drawer';
import List from '@material-ui/core/List'; import List from '@material-ui/core/List';
@ -10,6 +11,16 @@ import ExpandMore from '@material-ui/icons/ExpandMore';
import {makeStyles} from "@material-ui/core/styles"; import {makeStyles} from "@material-ui/core/styles";
import qs from 'query-string'; import qs from 'query-string';
import ListSubheader from "@material-ui/core/ListSubheader"; import ListSubheader from "@material-ui/core/ListSubheader";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import TextField from "@material-ui/core/TextField";
import DialogActions from "@material-ui/core/DialogActions";
import {Message, Upload} from 'element-react';
import {apiUrl} from "../utils/config";
const drawerWidth = 250; const drawerWidth = 250;
@ -20,13 +31,30 @@ const useStyles = makeStyles((theme) => ({
flexShrink: 0 flexShrink: 0
}, },
drawerPaper: { drawerPaper: {
height: 'calc(100% - 75px)',
zIndex: 1001, zIndex: 1001,
width: drawerWidth, width: drawerWidth,
top: 75 top: 75
}, },
button: {
width: 200,
position: 'fixed',
left: 25,
bottom: 25
},
list: {
height: 'calc(100% - 86px - 16px)',
overflowY: 'auto',
borderBottom: '1px solid #ccc',
paddingTop: '0'
},
nested: { nested: {
paddingLeft: theme.spacing(4) paddingLeft: theme.spacing(4)
}, },
nestedSubheader: {
paddingLeft: theme.spacing(4),
background: 'white'
},
menuLayer1: { menuLayer1: {
'& span': { '& span': {
fontWeight: 600, fontWeight: 600,
@ -52,6 +80,7 @@ function MenuBar (props) {
const classes = useStyles(); const classes = useStyles();
const [open, setOpen] = React.useState({game: true, agent: true}); const [open, setOpen] = React.useState({game: true, agent: true});
const handleClickGame = () => { const handleClickGame = () => {
setOpen({game: !open.game, agent: open.agent}); setOpen({game: !open.game, agent: open.agent});
}; };
@ -59,6 +88,46 @@ function MenuBar (props) {
setOpen({game: open.game, agent: !open.agent}); setOpen({game: open.game, agent: !open.agent});
}; };
const [uploadDialogOpen, setUploadDialogOpen] = React.useState(false);
const openUploadDialog = () => {
setUploadDialogOpen(true);
};
const handleUploadDialogClose = () => {
setUploadDialogOpen(false);
};
const uploadFormInitValue = {name: '', game: 'leduc-holdem', entry: ''};
const [uploadForm, setUploadForm] = React.useState({...uploadFormInitValue});
const handleUploadFormChange = (e, property) => {
let tempUploadForm = {...uploadForm};
tempUploadForm[property] = e.target.value;
setUploadForm(tempUploadForm);
}
let uploadRef = React.createRef();
const handleSubmitUpload = () => {
console.log(uploadForm, uploadRef);
const bodyFormData = new FormData();
bodyFormData.append('name', uploadForm.name);
bodyFormData.append('entry', uploadForm.entry);
bodyFormData.append('game', uploadForm.game);
bodyFormData.append('model', uploadRef.current.state.fileList[0].raw);
axios.post(`${apiUrl}/tournament/upload_agent`, bodyFormData, {headers: {'Content-Type': 'multipart/form-data'}})
.then(res => {
Message({
message: "Successfully uploaded model",
type: "success",
showClose: true,
});
props.setReloadMenu(props.reloadMenu+1);
setUploadDialogOpen(false);
setUploadForm({...uploadFormInitValue});
})
};
const history = useHistory(); const history = useHistory();
const handleGameJump = (gameName) => { const handleGameJump = (gameName) => {
history.push(`/leaderboard?type=game&name=${gameName}`); history.push(`/leaderboard?type=game&name=${gameName}`);
@ -97,7 +166,7 @@ function MenuBar (props) {
<List <List
component="nav" component="nav"
aria-labelledby="nested-list-subheader" aria-labelledby="nested-list-subheader"
className={classes.root} className={classes.list}
> >
<ListItem button onClick={handleClickAgent}> <ListItem button onClick={handleClickAgent}>
<ListItemText primary="Game LeaderBoards" className={classes.menuLayer1}/> <ListItemText primary="Game LeaderBoards" className={classes.menuLayer1}/>
@ -114,7 +183,7 @@ function MenuBar (props) {
{Object.keys(props.modelList).map(gameName => { {Object.keys(props.modelList).map(gameName => {
return ( return (
<div key={`agentMenu-sublist-${gameName}`}> <div key={`agentMenu-sublist-${gameName}`}>
<ListSubheader className={classes.nested}>{gameName}</ListSubheader> <ListSubheader className={classes.nestedSubheader}>{gameName}</ListSubheader>
{generateAgentMenu(props.modelList[gameName])} {generateAgentMenu(props.modelList[gameName])}
</div> </div>
) )
@ -122,6 +191,61 @@ function MenuBar (props) {
</Collapse> </Collapse>
</List> </List>
<Button variant="contained" color="primary" onClick={openUploadDialog} className={classes.button}>
Upload Model
</Button>
<Dialog open={uploadDialogOpen} onClose={handleUploadDialogClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Upload Model</DialogTitle>
<DialogContent>
<Upload
className="upload-demo"
drag
ref={uploadRef}
action="//placeholder/"
multiple
limit={1}
autoUpload={false}
>
<i className="el-icon-upload"/>
<div className="el-upload__text">Drag the file here, or <em>Click to upload</em></div>
</Upload>
<TextField
required
margin="dense"
id="name"
label="Model Name"
value={uploadForm.name}
onChange={(e) => handleUploadFormChange(e, 'name')}
fullWidth
/>
<TextField
required
margin="dense"
id="entry"
label="Entry"
value={uploadForm.entry}
onChange={(e) => handleUploadFormChange(e, 'entry')}
fullWidth
/>
<TextField
required
margin="dense"
id="game"
label="Game"
value={uploadForm.game}
onChange={(e) => handleUploadFormChange(e, 'game')}
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={handleUploadDialogClose} variant="contained" disableElevation>
Cancel
</Button>
<Button onClick={handleSubmitUpload} color="primary" variant="contained" disableElevation>
Upload
</Button>
</DialogActions>
</Dialog>
</Drawer> </Drawer>
) )
} }

View File

@ -209,7 +209,6 @@ class LeducHoldemGameView extends React.Component {
message: "Empty move history", message: "Empty move history",
type: "error", type: "error",
showClose: true, showClose: true,
duration: 0
}); });
this.setState({fullScreenLoading: false}); this.setState({fullScreenLoading: false});
return false; return false;

View File

@ -19,6 +19,9 @@ import TablePagination from "@material-ui/core/TablePagination";
import Breadcrumbs from "@material-ui/core/Breadcrumbs"; import Breadcrumbs from "@material-ui/core/Breadcrumbs";
import withStyles from "@material-ui/core/styles/withStyles"; import withStyles from "@material-ui/core/styles/withStyles";
import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline'; import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline';
import Button from "@material-ui/core/Button";
import {useHistory} from "react-router-dom";
import {Message} from "element-react";
const gameList = [ const gameList = [
{game: 'leduc-holdem', dispName: 'Leduc Hold\'em'}, {game: 'leduc-holdem', dispName: 'Leduc Hold\'em'},
@ -26,7 +29,6 @@ const gameList = [
]; ];
// {'doudizhu': ['agent1', 'agent2', 'agent3']} // {'doudizhu': ['agent1', 'agent2', 'agent3']}
const modelList = {};
function LeaderBoard () { function LeaderBoard () {
const initRowsPerPage = 10; const initRowsPerPage = 10;
@ -34,17 +36,31 @@ function LeaderBoard () {
const [page, setPage] = React.useState(0); const [page, setPage] = React.useState(0);
const [rowsTotal, setRowsTotal] = React.useState(0); const [rowsTotal, setRowsTotal] = React.useState(0);
const [rows, setRows] = React.useState([]); const [rows, setRows] = React.useState([]);
const [modelList, setModelList] = React.useState({});
const [defaultModelList, setDefaultModelList] = React.useState([]);
const [reloadMenu, setReloadMenu] = React.useState(0);
// passing an empty array as second argument triggers the callback in useEffect // passing an empty array as second argument triggers the callback in useEffect
// only after the initial render thus replicating `componentDidMount` lifecycle behaviour // only after the initial render thus replicating `componentDidMount` lifecycle behaviour
useEffect(() => { useEffect(() => {
gameList.forEach((game) => { async function fetchModelData() {
axios.get(`${apiUrl}/tournament/list_baseline_agents?game=${game.game}`) let tempModelList = {};
.then(res => { let tempDefaultModelList = [];
modelList[game.game] = res.data.data; for (const game of gameList) {
}); let res = await axios.get(`${apiUrl}/tournament/list_baseline_agents?game=${game.game}`);
}); console.log('agent', res);
}, []); tempModelList[game.game] = res.data.data;
tempDefaultModelList = tempDefaultModelList.concat(res.data.data);
res = await axios.get(`${apiUrl}/tournament/list_uploaded_agents?game=${game.game}`);
res.data.forEach(agentInfo => {
tempModelList[game.game].push(agentInfo.fields.name);
})
}
setModelList(tempModelList);
setDefaultModelList(tempDefaultModelList);
}
fetchModelData();
}, [reloadMenu]);
const { type, name } = qs.parse(window.location.search); const { type, name } = qs.parse(window.location.search);
let requestUrl = `${apiUrl}/tournament/`; let requestUrl = `${apiUrl}/tournament/`;
@ -76,10 +92,11 @@ function LeaderBoard () {
return ( return (
<div> <div>
<MenuBar gameList={gameList} modelList={modelList} /> <MenuBar gameList={gameList} modelList={modelList} reloadMenu={reloadMenu} setReloadMenu={setReloadMenu}/>
<div style={{marginLeft: '250px'}}> <div style={{marginLeft: '250px'}}>
<div style={{padding: 20}}> <div style={{padding: 20}}>
<EnhancedTable <EnhancedTable
defaultModelList={defaultModelList}
tableRows={rows} tableRows={rows}
routeInfo={{type, name}} routeInfo={{type, name}}
page={page} page={page}
@ -88,6 +105,8 @@ function LeaderBoard () {
setRowsPerPage={(q) => {setRowsPerPage(q)}} setRowsPerPage={(q) => {setRowsPerPage(q)}}
rowsTotal={rowsTotal} rowsTotal={rowsTotal}
type={type} type={type}
reloadMenu={reloadMenu}
setReloadMenu={setReloadMenu}
/> />
</div> </div>
</div> </div>
@ -163,7 +182,9 @@ const useToolbarStyles = makeStyles((theme) => ({
paddingLeft: theme.spacing(2), paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1), paddingRight: theme.spacing(1),
minHeight: 52, minHeight: 52,
justifyContent: 'space-between'
}, },
button: {},
highlight: highlight:
theme.palette.type === 'light' theme.palette.type === 'light'
? { ? {
@ -183,7 +204,8 @@ const useToolbarStyles = makeStyles((theme) => ({
})); }));
const EnhancedTableToolbar = (props) => { const EnhancedTableToolbar = (props) => {
const { routeInfo } = props; const { routeInfo, defaultModelList, reloadMenu, setReloadMenu } = props;
console.log('defaultModelList', defaultModelList)
const classes = useToolbarStyles(); const classes = useToolbarStyles();
let name = ''; let name = '';
@ -196,6 +218,54 @@ const EnhancedTableToolbar = (props) => {
name = routeInfo.name; name = routeInfo.name;
} }
const history = useHistory();
const functionalButton = () => {
if (routeInfo.type === 'game'){
const handleLaunchTournament = (gameName) => {
// todo: customize eval num
// todo: add global loading when waiting for API response
axios.get(`${apiUrl}/tournament/launch?eval_num=200&name=${gameName}`)
.then(res => {
Message({
message: "Successfully launched tournament",
type: "success",
showClose: true
});
})
}
return (
<div className={classes.button}>
<Button variant="contained" color="primary" onClick={() => handleLaunchTournament(routeInfo.name)}>
Launch Tournament
</Button>
</div>
)
}
else if (routeInfo.type ==='agent') {
const delButtonDisabled = defaultModelList.includes(routeInfo.name);
const handleDelModel = (agentName) => {
axios.get(`${apiUrl}/tournament/delete_agent?name=${agentName}`)
.then(res => {
Message({
message: "Successfully deleted model",
type: "success",
showClose: true
});
setReloadMenu(reloadMenu+1);
history.push(`/leaderboard?type=game&name=leduc-holdem`);
})
};
return (
<div className={classes.button}>
<Button variant="contained" onClick={() => handleDelModel(routeInfo.name)} color="primary" disabled={delButtonDisabled}>
Delete Model
</Button>
</div>
)
}
}
return ( return (
<Toolbar <Toolbar
className={classes.root} className={classes.root}
@ -209,6 +279,9 @@ const EnhancedTableToolbar = (props) => {
</Typography> </Typography>
<Typography color="textPrimary">{name}</Typography> <Typography color="textPrimary">{name}</Typography>
</Breadcrumbs> </Breadcrumbs>
<div className={classes.button}>
{functionalButton()}
</div>
</Toolbar> </Toolbar>
); );
}; };
@ -330,7 +403,12 @@ const AgentTableContent = (props) => {
const EnhancedTable = (props) => { const EnhancedTable = (props) => {
const { tableRows, routeInfo, rowsPerPage, page, setPage, setRowsPerPage, rowsTotal, type } = props; const { tableRows, routeInfo,
rowsPerPage, page, setPage,
setRowsPerPage, rowsTotal,
type, defaultModelList,
reloadMenu, setReloadMenu
} = props;
const classes = useStyles(); const classes = useStyles();
const handleChangePage = (event, newPage) => { const handleChangePage = (event, newPage) => {
@ -352,7 +430,7 @@ const EnhancedTable = (props) => {
return ( return (
<div className={classes.root}> <div className={classes.root}>
<Paper className={classes.paper}> <Paper className={classes.paper}>
<EnhancedTableToolbar routeInfo={routeInfo}/> <EnhancedTableToolbar routeInfo={routeInfo} defaultModelList={defaultModelList} reloadMenu={reloadMenu} setReloadMenu={setReloadMenu}/>
{tableContent} {tableContent}
<TablePagination <TablePagination
// todo: remove testing page size option // todo: remove testing page size option