Skip to content

Commit

Permalink
Score snapshot and settings
Browse files Browse the repository at this point in the history
  • Loading branch information
AntonUden committed Nov 9, 2023
1 parent bcf1a1c commit 86b9223
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public AbstractHTTPResponse handleRequest(Request request, Authentication authen

playerIds.stream().filter(p -> p.getUuid().toString().equalsIgnoreCase(uuidString)).findFirst().ifPresent(pid -> {
try {
String server = player.getString("server");
String server = player.optString("server", "");
String reason = player.getString("reason");
int amount = player.getInt("amount");
String gainedAt = player.getString("gained_at");
Expand Down Expand Up @@ -136,7 +136,7 @@ public AbstractHTTPResponse handleRequest(Request request, Authentication authen

teamIds.stream().filter(t -> t.getTeamNumber() == teamNumber).findFirst().ifPresent(tid -> {
try {
String server = team.getString("server");
String server = team.optString("server", "");
String reason = team.getString("reason");
int amount = team.getInt("amount");
String gainedAt = team.getString("gained_at");
Expand Down
2 changes: 2 additions & 0 deletions ReactUI/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import EditorProvider from './components/EditorProvider';

/// @ts-ignore
import catCry from "./assets/img/cat_cry.png";
import ScoreSnapshot from './pages/ScoreSnapshot';

export default function App() {
const tournamentSystem = useTournamentSystemContext();
Expand Down Expand Up @@ -63,6 +64,7 @@ export default function App() {
<Route path="/chat" element={<AuthenticatedZone><ChatLog /></AuthenticatedZone>} />
<Route path="/accounts" element={<AuthenticatedZone><Accounts /></AuthenticatedZone>} />
<Route path="/editor" element={<AuthenticatedZone><EditorProvider /></AuthenticatedZone>} />
<Route path="/score_snapshot" element={<AuthenticatedZone><ScoreSnapshot /></AuthenticatedZone>} />

{/* Unauthenticated zones */}
<Route path="/live_stats" element={<LiveStats />} />
Expand Down
2 changes: 1 addition & 1 deletion ReactUI/src/components/modals/ServerSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default function ServerSelector({ visible, text, title = "Select server",
}, []);

useEffect(() => {
console.debug("Resetting server picker modal");
//console.debug("Resetting server picker modal");
if (servers.length > 0) {
setServer(servers[0].name);
}
Expand Down
7 changes: 4 additions & 3 deletions ReactUI/src/components/modals/TextPromptModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,18 @@ interface Props {
extraButtonText?: string;
extraButtonType?: string;
allowEnterToSubmit?: boolean;
initialValue?: string;
onExtraButtonClick?: () => void;
onClose: () => void;
onSubmit: (text: string) => void;
}

export default function TextPromptModal({ maxLength, placeholder, children, visible, title, onClose, onSubmit, allowEnterToSubmit = true, cancelText = "Cancel", confirmText = "Confirm", extraButtonVisible = false, extraButtonText = "Extra button", extraButtonType = "secondary", onExtraButtonClick = () => { }, cancelType = "secondary", confirmType = "primary" }: Props) {
export default function TextPromptModal({ initialValue = "", maxLength, placeholder, children, visible, title, onClose, onSubmit, allowEnterToSubmit = true, cancelText = "Cancel", confirmText = "Confirm", extraButtonVisible = false, extraButtonText = "Extra button", extraButtonType = "secondary", onExtraButtonClick = () => { }, cancelType = "secondary", confirmType = "primary" }: Props) {
const [text, setText] = useState<string>("");

useEffect(() => {
console.debug("Resetting text prompt modal");
setText("");
//console.debug("Resetting text prompt modal");
setText(initialValue);
}, [visible]);


Expand Down
7 changes: 4 additions & 3 deletions ReactUI/src/components/modals/console/ServerConsoleModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import "./ServerConsoleModal.scss";
import StartServerButton from '../../buttons/server/StartServerButton';
import KillServerButton from '../../buttons/server/KillServerButton';
import toast from 'react-hot-toast';
import { Permission } from '../../../scripts/enum/Permission';

interface Props {
visible: boolean;
Expand Down Expand Up @@ -178,7 +179,7 @@ export default function ServerConsoleModal({ server, visible, onClose }: Props)
}

async function executeCommand() {
if(command.trim().length == 0) {
if (command.trim().length == 0) {
return;
}

Expand Down Expand Up @@ -216,8 +217,8 @@ export default function ServerConsoleModal({ server, visible, onClose }: Props)
</ModalBody>
<ModalFooter>
<InputGroup>
<FormControl type='text' value={command} onChange={handleCommandChange} onKeyDown={handleKeyDown} placeholder='Enter command. Press enter key to run' />
<Button variant="primary" onClick={executeCommand}>Send</Button>
<FormControl type='text' value={command} onChange={handleCommandChange} onKeyDown={handleKeyDown} placeholder='Enter command. Press enter key to run' disabled={!tournamentSystem.authManager.hasPermission(Permission.REMOTE_EXECUTE_SERVER_COMMAND)} />
<Button variant="primary" onClick={executeCommand} disabled={!tournamentSystem.authManager.hasPermission(Permission.REMOTE_EXECUTE_SERVER_COMMAND)}>Send</Button>
{server.is_running ?
<KillServerButton server={server} />
:
Expand Down
1 change: 1 addition & 0 deletions ReactUI/src/components/nav/PageSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default function PageSelection() {
<Nav className='my-1 mx-2' variant='tabs'>
<PageNavLink url="/" text="Overview" />
<PageNavLink url="/score" text="Score" />
<PageNavLink url="/score_snapshot" text="Score Snapshot" />
<PageNavLink url="/triggers" text="Triggers" />
<PageNavLink url="/servers" text="Servers" />
<PageNavLink url="/whitelist" text="Whitelist" />
Expand Down
103 changes: 103 additions & 0 deletions ReactUI/src/components/navbar/GlobalNavbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Permission } from '../../scripts/enum/Permission'
import toast from 'react-hot-toast'
import ConfirmModal from '../modals/ConfirmModal'
import { LocalStorageKeys } from '../../scripts/enum/LocalStorageKeys'
import TextPromptModal from '../modals/TextPromptModal'

interface Props {
loggedIn: boolean
Expand Down Expand Up @@ -69,6 +70,83 @@ export default function GlobalNavbar({ loggedIn }: Props) {
}
}

const [nameModalOpen, setNameModalOpen] = useState<boolean>(false);
const [motdModalOpen, setMOTDModalOpen] = useState<boolean>(false);
const [urlModalOpen, setUrlModalOpen] = useState<boolean>(false);


function openSetTournamentName() {
if (!tournamentSystem.authManager.hasPermission(Permission.MANAGE_SETTINGS)) {
toast.error("You dont have permission to manage this setting");
return;
}
setNameModalOpen(true);
}

function openSetMOTD() {
if (!tournamentSystem.authManager.hasPermission(Permission.MANAGE_SETTINGS)) {
toast.error("You dont have permission to manage this setting");
return;
}
setMOTDModalOpen(true);
}

function openSetScoreboardURL() {
if (!tournamentSystem.authManager.hasPermission(Permission.MANAGE_SETTINGS)) {
toast.error("You dont have permission to manage this setting");
return;
}
setUrlModalOpen(true);
}

async function onSetName(name: string) {
const response = await tournamentSystem.api.setTournamentName(name);
if (response.success) {
toast.success("Tournament name set. You might have to restart for it to be applied");
setNameModalOpen(false);
} else {
console.error("Failed to set tournament name. " + response.message);
toast.error("Failed to set tournament name. " + response.message);
}
}

async function onSetMOTD(motd: string) {
const response = await tournamentSystem.api.setMOTD(motd);
if (response.success) {
toast.success("MOTD set");
setMOTDModalOpen(false);
} else {
console.error("Failed to set tournament name. " + response.message);
toast.error("Failed to set tournament name. " + response.message);
}
}

async function onSetUrl(url: string) {
const response = await tournamentSystem.api.setScoreboardURL(url);
if (response.success) {
toast.success("Scoreboard URL set. You might have to restart for it to be applied");
setUrlModalOpen(false);
} else {
console.error("Failed to set scoreboard url. " + response.message);
toast.error("Failed to set scoreboard url. " + response.message);
}
}

async function reloadDynamicConfig() {
const response = await tournamentSystem.api.reloadDynamicConfig();
if (response.success) {
if (response.data.success) {
toast.success("Dynamic config reloaded");
} else {
console.error(response.data.message);
toast.error(response.data.message);
}
} else {
console.error("Failed to reload dynamic config. " + response.message);
toast.error("Failed to reload dynamic config. " + response.message);
}
}

return (
<>
<Navbar expand="lg" bg="dark" data-bs-theme="dark">
Expand All @@ -92,9 +170,16 @@ export default function GlobalNavbar({ loggedIn }: Props) {
<NavLink as={Link} to="/editor">Editor</NavLink>
</NavItem>
<NavDropdown title="System" id="basic-nav-dropdown">
<DropdownItemText>Settings</DropdownItemText>
<DropdownItem onClick={openSetTournamentName}>Set tournament name</DropdownItem>
<DropdownItem onClick={openSetMOTD}>Set MOTD</DropdownItem>
<DropdownItem onClick={openSetScoreboardURL}>Set scoreboard url</DropdownItem>
<DropdownDivider />
<DropdownItemText>Account management</DropdownItemText>
<DropdownItem as={Link} to="/accounts">Manage accounts</DropdownItem>
<DropdownDivider />
<DropdownItemText>Management</DropdownItemText>
<DropdownItem onClick={reloadDynamicConfig}>Reload dynamic config</DropdownItem>
<DropdownItem onClick={openResetPropmpt} className='text-danger'>Reset</DropdownItem>
<DropdownItem onClick={openShutdownPropmpt} className='text-danger'>Shutdown</DropdownItem>
</NavDropdown>
Expand Down Expand Up @@ -124,6 +209,24 @@ export default function GlobalNavbar({ loggedIn }: Props) {
</p>
</ConfirmModal>

<TextPromptModal onClose={() => { setNameModalOpen(false) }} initialValue={tournamentSystem.state.system.tournament_name} onSubmit={onSetName} title='Set tournament name' visible={nameModalOpen} cancelText='Cancel' cancelType='secondary' confirmType='primary' confirmText='Set name' placeholder='Tournament name'>
<p>
Enter the new tournament name
</p>
</TextPromptModal>

<TextPromptModal onClose={() => { setMOTDModalOpen(false) }} initialValue={tournamentSystem.state.system.motd} onSubmit={onSetMOTD} title='Set MOTD' visible={motdModalOpen} cancelText='Cancel' cancelType='secondary' confirmType='primary' confirmText='Set MOTD' placeholder='MOTD'>
<p>
Enter the new MOTD
</p>
</TextPromptModal>

<TextPromptModal onClose={() => { setUrlModalOpen(false) }} initialValue={tournamentSystem.state.system.scoreboard_url} onSubmit={onSetUrl} title='Set scoreboard url' visible={urlModalOpen} cancelText='Cancel' cancelType='secondary' confirmType='primary' confirmText='Set URL' placeholder='Scoreboard URL'>
<p>
Enter the new scoreboard url
</p>
</TextPromptModal>

<ThemeSelector onClose={closeThemeSelector} visible={themeSelectorVisible} />
</>
)
Expand Down
106 changes: 106 additions & 0 deletions ReactUI/src/pages/ScoreSnapshot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { ChangeEvent, useState } from 'react'
import { Button, Col, Container, FormControl, FormGroup, FormLabel, Row } from 'react-bootstrap'
import { useTournamentSystemContext } from '../context/TournamentSystemContext';
import toast from 'react-hot-toast';
import PageSelection from '../components/nav/PageSelection';
import { Permission } from '../scripts/enum/Permission';

export default function ScoreSnapshot() {
const tournamentSystem = useTournamentSystemContext();

const [data, setData] = useState<string>("");

function handleDataChange(e: ChangeEvent<any>) {
setData(e.target.value);
}

function handleFileChange(e: ChangeEvent<any>) {
const file = e.target.files[0];

if (file) {
const reader = new FileReader();
reader.onload = (r) => {
const content = r.target!.result;
setData(String(content));
toast.success("File loaded");
};
reader.readAsText(file);
}
};

async function exportData() {
const data = await tournamentSystem.api.exportScoreSnapshot();

if (data.success) {
let dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data.data, null, 4));
let downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", "TournamentScoreSnapshot.json");
document.body.appendChild(downloadAnchorNode); // required for firefox
downloadAnchorNode.click();
downloadAnchorNode.remove();

toast.success("Score snapshot downloaded");
} else {
console.error("Failed to export snapshot. " + data.message);
toast.error("Failed to export snapshot. " + data.message);
}
}

async function importData() {
if (data.trim().length == 0) {
toast.error("Pleas paste JSON or import snapshot file first");
return;
}

let json: any;
try {
json = JSON.parse(data);
} catch (err) {
console.log("Failed to parse json");
console.error(err);
toast.error("Failed to parse data. Please check that the input is valid json and try again");
return;
}

if (!Array.isArray(json.players) || !Array.isArray(json.teams)) {
toast.error("The provided data does not seem to be valid score anspshot data");
return;
}

const response = await tournamentSystem.api.importScoreSnapshot(json);
if (response.success) {
toast.success("Score imported");
} else {
console.error("Failed to import score snapshot: " + response.message);
toast.error("Failed to import score snapshot, please verify that valid data was provided. " + response.message);
}
}

return (
<Container fluid>
<PageSelection />

<Row>
<Col>
<Button variant='success' onClick={exportData} className='mx-2 my-2'>Export snapshot</Button>
<Button variant='success' onClick={importData} className='my-2' disabled={!tournamentSystem.authManager.hasPermission(Permission.IMPORT_SCORE_SNAPSHOT)}>Import snapshot</Button>
</Col>
</Row>

<Row>
<Col>
<FormGroup>
<FormLabel>Pase JSON here</FormLabel>
<FormControl as="textarea" rows={10} value={data} onChange={handleDataChange} />
</FormGroup>
<hr />
<FormGroup>
<FormLabel>Or upload JSON file with score data</FormLabel>
<FormControl type='file' onChange={handleFileChange} />
</FormGroup>
</Col>
</Row>
</Container>
)
}
Loading

0 comments on commit 86b9223

Please sign in to comment.