Skip to content
This repository has been archived by the owner on Feb 1, 2024. It is now read-only.

Commit

Permalink
Basic API message capture
Browse files Browse the repository at this point in the history
  • Loading branch information
mjaquiery committed Feb 28, 2023
1 parent 363da59 commit dae48e2
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 111 deletions.
61 changes: 38 additions & 23 deletions frontend/src/APIConnection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import {UpdateResult} from "./UserProfile";

export type User = {
url: string;
id: number;
Expand Down Expand Up @@ -36,6 +34,19 @@ export type CachedAPIResponse<T extends SingleAPIResponse> = {
parents: string[];
}

export type APIMessage = {
severity: "error" | "warning" | "info" | "success";
message: string;
context?: any;
}

export type MessageHandler = (message: APIMessage) => void

export type ConnectionProps = {
base_url?: string;
message_handlers?: MessageHandler[];
}

function clean_url(url: string, baseURL: string): string {
url = url.toLowerCase();
if (!url.startsWith(baseURL))
Expand Down Expand Up @@ -97,15 +108,29 @@ class ResponseCache {
}
}

/**
* @class
* APIConnection manages a connection to the backend REST API.
* It handles user management directly, and offers a modified
* fetch interface for everything else.
*
* Results are cached and can be referenced directly with their
* canonical URL and indirectly with a parent URL.
*/
export class APIConnection {
url: string = 'http://localhost:5000/'.toLowerCase()
user: User | null = null
cache_expiry_time = 60_000 // 1 minute
results: ResponseCache
cookies: any
message_handlers: MessageHandler[] = []

constructor() {
console.info("Spawn API connection")
constructor(props?: ConnectionProps) {
if (props?.base_url)
this.url = props.base_url
if (props?.message_handlers)
this.message_handlers = props.message_handlers
console.info(`Spawn API connection (${this.url})`)
const local_user = window.localStorage.getItem('user')
if (local_user)
this.user = JSON.parse(local_user)
Expand All @@ -128,7 +153,7 @@ export class APIConnection {
})
}

update_user(email: string, password: string, currentPassword: string): Promise<UpdateResult> {
update_user(email: string, password: string, currentPassword: string): Promise<APIMessage> {
if (!this.user)
return new Promise(() => {})
return fetch(
Expand All @@ -149,9 +174,9 @@ export class APIConnection {
.then(user => {
this.user = {...user, token: this.user?.token}
})
.then(() => ({success: true, message: 'Updated successfully'}))
.then(() => ({severity: "success", message: 'Updated successfully'}))
return r.json()
.then(r => ({success: false, message: r.error}))
.then(r => ({severity: "error", message: r.error}))
})
}

Expand Down Expand Up @@ -179,26 +204,10 @@ export class APIConnection {
return fetch(this.url + "logout/", {method: 'POST'})
}

get_cookie(name: string) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts?.pop()?.split(';').shift();
return null
}

get is_logged_in() {
return !!this.user?.token
}

async get_is_logged_in(skip: boolean = true): Promise<boolean> {
await this.login('admin', 'admin')
const cookie = this.get_cookie('csrf_access_token');
if (cookie === undefined || !this.user)
if (!skip)
return this.get_is_logged_in(false);
return cookie !== undefined && this.user !== null;
}

_prepare_fetch_headers(url: string, options?: any) {
if (!this.is_logged_in)
throw new Error(`Cannot fetch ${url}: not logged on.`)
Expand Down Expand Up @@ -230,9 +239,15 @@ export class APIConnection {
return null;
return response.json() as Promise<T|T[]>;
})
.catch(e => {
this.message_handlers.forEach(h => h({severity: "error", message: e.message, context: e}))
return url
})
.then(json => {
if (void_cache)
this.results.purge(parent)
if (typeof json === "string")
return json
if (json === null) {
this.results.remove(url)
return url
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/AsyncTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export default class AsyncTable<T extends APIObject> extends Component<AsyncTabl
)
}

update_all = async (use_cache: boolean = true) => {
update_all = async (use_cache: boolean = false) => {
this.reset_new_row()
return this.get_data(this.props.url, use_cache)
}
Expand Down
178 changes: 99 additions & 79 deletions frontend/src/Core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import { ReactComponent as GalvanalyserLogo } from './Galvanalyser-logo.svg';
import Connection from "./APIConnection";
import Connection, {APIMessage} from "./APIConnection";
import Stack from "@mui/material/Stack";
import Tokens from "./Tokens";
import UserProfile from "./UserProfile";
import Snackbar from "@mui/material/Snackbar";
import Alert from "@mui/material/Alert";

const PrivateRoute = (component: JSX.Element) => {
const logged = Connection.is_logged_in;
Expand Down Expand Up @@ -82,7 +84,7 @@ const useStyles = makeStyles((theme) => ({
}),
},
galvanalyserLogo: {
height: '40px'
height: '40px'
},
menuButton: {
marginRight: 36,
Expand Down Expand Up @@ -164,35 +166,48 @@ export default function Core() {
setOpen(false);
};

const [snackbarOpen, setSnackbarOpen] = React.useState<boolean>(false)
const [apiMessage, setAPIMessage] = React.useState<APIMessage|null>(null)
const handleSnackbarClose = (e: any, reason?: string) => {
if (reason !== 'clickaway') {
setSnackbarOpen(false)
setAPIMessage(null)
}
}
Connection.message_handlers.push((h) => {
setAPIMessage(h)
setSnackbarOpen(true)
})

const mainListItems = (
<Stack>
<ListItem button
selected={isDatasetPath}
component={Link} to={datasetsPath}>
<ListItem button
selected={isDatasetPath}
component={Link} to={datasetsPath}>
<ListItemIcon>
<TableChartIcon />
</ListItemIcon>
<ListItemText primary="Datasets" />
</ListItem>
<ListItem button
selected={isHarvestersPath}
component={Link} to={harvestersPath}>
<ListItem button
selected={isHarvestersPath}
component={Link} to={harvestersPath}>
<ListItemIcon>
<BackupIcon />
</ListItemIcon>
<ListItemText primary="Harvesters" />
</ListItem>
<ListItem button
selected={isCellsPath}
component={Link} to={cellsPath}>
<ListItem button
selected={isCellsPath}
component={Link} to={cellsPath}>
<ListItemIcon>
<BatteryUnknownIcon />
</ListItemIcon>
<ListItemText primary="Cells" />
</ListItem>
<ListItem button
selected={isEquipmentPath}
component={Link} to={equipmentPath}>
<ListItem button
selected={isEquipmentPath}
component={Link} to={equipmentPath}>
<ListItemIcon>
<SpeedIcon/>
</ListItemIcon>
Expand All @@ -214,78 +229,83 @@ export default function Core() {

const Layout = (
<div className={classes.root}>
<CssBaseline />
<AppBar position="absolute" className={clsx(classes.appBar, open && classes.appBarShift)}>
<Toolbar className={classes.toolbar}>
<IconButton
edge="start"
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
className={clsx(classes.menuButton, open && classes.menuButtonHidden)}
>
<MenuIcon />
</IconButton>
<GalvanalyserLogo className={classes.galvanalyserLogo}/>
<Typography component="h1" variant="h6" color="inherit" noWrap className={classes.title}>
</Typography>
<CssBaseline />
<AppBar position="absolute" className={clsx(classes.appBar, open && classes.appBarShift)}>
<Toolbar className={classes.toolbar}>
<IconButton
edge="start"
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
className={clsx(classes.menuButton, open && classes.menuButtonHidden)}
>
<MenuIcon />
</IconButton>
<GalvanalyserLogo className={classes.galvanalyserLogo}/>
<Typography component="h1" variant="h6" color="inherit" noWrap className={classes.title}>
</Typography>

<Button color="inherit" >
User: {userDisplayName}
</Button>
<Button color="inherit" onClick={() => {
navigate(profilePath)
}}>
Manage Profile
</Button>
<Button color="inherit" onClick={() => {
navigate(tokenPath)
}}>
Manage API Tokens
</Button>
<Button color="inherit" onClick={() => {
Connection.logout().then(()=> {navigate('/login');});
}}>
Logout
</Button>
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
classes={{
paper: clsx(classes.drawerPaper, !open && classes.drawerPaperClose),
}}
open={open}
>
<div className={classes.toolbarIcon}>
<IconButton onClick={handleDrawerClose}>
<ChevronLeftIcon />
</IconButton>
</div>
<Divider />
<List>{mainListItems}</List>
</Drawer>
<main className={classes.content}>
<div className={classes.appBarSpacer} />
<Button color="inherit" >
User: {userDisplayName}
</Button>
<Button color="inherit" onClick={() => {
navigate(profilePath)
}}>
Manage Profile
</Button>
<Button color="inherit" onClick={() => {
navigate(tokenPath)
}}>
Manage API Tokens
</Button>
<Button color="inherit" onClick={() => {
Connection.logout().then(()=> {navigate('/login');});
}}>
Logout
</Button>
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
classes={{
paper: clsx(classes.drawerPaper, !open && classes.drawerPaperClose),
}}
open={open}
>
<div className={classes.toolbarIcon}>
<IconButton onClick={handleDrawerClose}>
<ChevronLeftIcon />
</IconButton>
</div>
<Divider />
<List>{mainListItems}</List>
</Drawer>
<main className={classes.content}>
<div className={classes.appBarSpacer} />
<Outlet />
</main>
</main>
<Snackbar open={snackbarOpen} autoHideDuration={6000} onClose={handleSnackbarClose}>
<Alert onClose={handleSnackbarClose} severity={apiMessage?.severity} sx={{ width: '100%' }}>
{apiMessage?.message}
</Alert>
</Snackbar>
</div>
);

/* A <Routes> looks through its children <Route>s and renders the first one that matches the current URL. */
return (
<Routes>
<Route path="/login" element={<Login />}/>
<Route path={datasetsPath} element={PrivateRoute(Layout)}>
<Route path={cellsPath} element={Cells()} />
<Route path={equipmentPath} element={Equipment()} />
<Route path={harvestersPath} element={Harvesters()} />
<Route path={usersPath} element={ActivateUsers()} />
<Route path={profilePath} element={UserProfile()} />
<Route path={tokenPath} element={Tokens()} />
<Route index element={Datasets()} />
</Route>
</Routes>
<Routes>
<Route path="/login" element={<Login />}/>
<Route path={datasetsPath} element={PrivateRoute(Layout)}>
<Route path={cellsPath} element={Cells()} />
<Route path={equipmentPath} element={Equipment()} />
<Route path={harvestersPath} element={Harvesters()} />
<Route path={usersPath} element={ActivateUsers()} />
<Route path={profilePath} element={UserProfile()} />
<Route path={tokenPath} element={Tokens()} />
<Route index element={Datasets()} />
</Route>
</Routes>
);
}

Expand Down
Loading

0 comments on commit dae48e2

Please sign in to comment.