You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

404 lines
19 KiB

import React, {useEffect, useRef, useState} from 'react';
import FormControl from "@material-ui/core/FormControl";
import {makeStyles} from "@material-ui/core/styles";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import Autocomplete from '@material-ui/lab/Autocomplete';
import TextField from "@material-ui/core/TextField";
import Grid from "@material-ui/core/Grid";
import {BugReport, Close, Folder, Launch, MoodBadTwoTone, Settings} from "@material-ui/icons";
import Alert from "@material-ui/lab/Alert";
import {Card, CircularProgress} from "@material-ui/core";
import Avatar from "@material-ui/core/Avatar";
import CardHeader from "@material-ui/core/CardHeader";
import Button from "@material-ui/core/Button";
import createMuiTheme from "@material-ui/core/styles/createMuiTheme";
import {ThemeProvider} from "@material-ui/styles";
import Popper from "@material-ui/core/Popper";
import CardContent from "@material-ui/core/CardContent";
import IconButton from "@material-ui/core/IconButton";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import InputLabel from "@material-ui/core/InputLabel";
import whiteIcon from './whiteicon.png';
import blueIcon from './blueicon.png';
import Console from "./components/Console";
const useStyles = makeStyles(theme => ({
formControl: {
margin: theme.spacing(1),
color: '#fff',
flexGrow: 1,
},
selectEmpty: {
marginTop: theme.spacing(2),
},
title: {
flexGrow: 1,
"& small": {
fontSize: 12,
opacity: 0.7,
verticalAlign: "text-top"
}
},
cardHeaderTitle: {
minWidth: 10
},
inputRoot: {
color: "white",
"& .MuiOutlinedInput-notchedOutline": {
borderColor: "white"
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: "white"
},
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: "white"
},
},
endAdornment: {
"& .MuiButtonBase-root": {
color: "white"
}
},
inputLabelRoot: {
color: "white"
}
}));
const theme = createMuiTheme({
palette: {
primary: {main: "rgb(50,109,230)"},
secondary: {main: "#ccc"}
},
});
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function App() {
const classes = useStyles();
const [namespaces, setNamespaces] = useState([]);
const [selectedNamespaces, setSelectedNS] = useState([]);
const [anchorEl, setAnchorEl] = useState(null);
const prevSelectedNamespaces = usePrevious(selectedNamespaces);
const [configFilePath, setConfigFilePath] = useState(null)
const confPathEl = useRef(null)
const [loading, setLoading] = useState(true);
const [websites, setWebsites] = useState([]);
const [showConfig, setShowConfig] = useState(false);
const [configMessage, setConfigMessage] = useState(null);
const [availableContexts, setAvailableContexts] = useState([]);
const [currentContext, setCurrentContext] = useState(null);
const [version, setVersion] = useState(null);
const [showConsole, setShowConsole] = useState(false);
// const prevContext = usePrevious(currentContext);
console.log()
useEffect(() => {
window.backend.Client.GetCurrentConfigPath().then(cp => {
if (cp) {
setConfigFilePath(cp);
refreshContext();
} else {
Promise.all([window.backend.Client.GetAvailableContexts(), window.backend.Client.GetCurrentContext()]).then(([acs, cc]) => {
console.log("current context", cc, ", available:", acs);
setAvailableContexts(acs);
setCurrentContext(cc);
});
}
});
window.backend.PortfallOS.GetVersion().then(v => {
setVersion(v);
})
}, []);
const refreshContext = () => {
setWebsites([]);
setLoading(true);
window.backend.Client.ListNamespaces().then((r) => {
setNamespaces(r);
setSelectedNS(["default"]);
setLoading(false)
});
Promise.all([window.backend.Client.GetAvailableContexts(), window.backend.Client.GetCurrentContext()]).then(([acs, cc]) => {
console.log("current context", cc, ", available:", acs);
setAvailableContexts(acs);
setCurrentContext(cc);
});
}
useEffect(() => {
if (selectedNamespaces !== null && prevSelectedNamespaces !== selectedNamespaces) {
setLoading(true);
// get ns to add
const nsToAdd = selectedNamespaces.find(ns => !(prevSelectedNamespaces || []).includes(ns));
const namespacesToRemove = (prevSelectedNamespaces || []).filter(ns => !selectedNamespaces.includes(ns));
let newWebsites = websites;
if (namespacesToRemove) {
Promise.all(namespacesToRemove.map(ns => window.backend.Client.RemoveWebsitesInNamespace(ns))).then(() => {
console.log("removed namespaces", namespacesToRemove)
});
newWebsites = websites.filter(w => {
if (namespacesToRemove.includes(w.namespace)) {
return false
}
if (namespacesToRemove.includes("All Namespaces") && !selectedNamespaces.includes(w.namespace)) {
return false
}
return true
});
}
if (nsToAdd) {
console.log("adding ns", nsToAdd)
window.backend.Client.GetWebsitesInNamespace(nsToAdd).then(results => {
console.log("received websites in ns to add", results)
// when we already have all namespaces there is no need to concat the results of the given website
if (results && !(prevSelectedNamespaces || []).includes("All Namespaces")) {
const resObj = JSON.parse(results)
if (resObj) {
newWebsites = newWebsites.concat(resObj);
}
}
// if we just added All namespaces the portforwards will be reset so we sh
setWebsites(newWebsites);
setLoading(false);
});
} else {
setWebsites(newWebsites);
setLoading(false)
}
}
}, [selectedNamespaces, prevSelectedNamespaces, websites]);
return (
<ThemeProvider theme={theme}>
<img src={blueIcon} style={{
position: "absolute",
top: 0,
bottom: 0,
left: 0,
right: 0,
margin: "auto",
width: "60%",
opacity: 0.02
}} alt="Blue Portfall logo in background"/>
<div id="app" className="App">
<AppBar id="Controls">
<Toolbar>
<img alt="White Portfall logo in toolbar" src={whiteIcon}
style={{width: 40, height: 40, marginRight: "1em"}}/>
<Typography className={classes.title} variant="h6" noWrap>
Portfall {version ? <small>{version}</small> : null}
</Typography>
{/* todo: arrange by namespace */}
<FormControl className={classes.formControl}>
<Autocomplete options={["All Namespaces"].concat(namespaces)}
multiple
value={selectedNamespaces}
classes={{
inputRoot: classes.inputRoot,
endAdornment: classes.endAdornment
}}
onChange={(_, value) => {
//
setSelectedNS(value)
}}
renderInput={params => (
<TextField {...params} style={{color: 'white'}}
label="Namespaces"
InputLabelProps={{
color: "secondary",
classes: {root: classes.inputLabelRoot}
}}
variant="outlined"/>)
}/>
</FormControl>
</Toolbar>
</AppBar>
<div id="Websites" style={{
paddingTop: 100,
flex: 1,
alignItems: 'center',
justifyContent: 'center',
display: 'flex'
}}>
<Grid container spacing={3} style={{
flexGrow: 1,
padding: "0 2rem"
}}>
{!configFilePath ? (<Grid item xs={12}>
<Alert icon={<MoodBadTwoTone/>} severity="error">
<Typography>No config file found</Typography>
</Alert>
</Grid>) : null}
{(configFilePath && websites.length === 0 && namespaces.length) ? (<Grid item xs={12}>
<Alert icon={<MoodBadTwoTone/>} severity="info">
<Typography>No websites found to port-forward in the selected namespace(s)</Typography>
</Alert>
</Grid>) : null}
{(configFilePath && !namespaces.length) ? (<Grid item xs={12}>
<Alert icon={<MoodBadTwoTone/>} severity="warning">
<Typography>Invalid context, try updating your config or switching context</Typography>
</Alert>
</Grid>) : null}
{websites.map(({localPort, podPort, title, iconRemoteUrl}) => (
<Grid item xs={4} key={localPort}>
<Card>
<CardHeader classes={{content: classes.cardHeaderTitle}}
avatar={<Avatar src={iconRemoteUrl}/>}
title={<Typography noWrap>{title}</Typography>}
subheader={<span><b>{localPort}</b>:{podPort}</span>} action={
<Button endIcon={<Launch/>} size="small" color="primary"
onClick={() =>
window.backend.PortfallOS.OpenInBrowser(`http://localhost:${localPort}`)}>
Open
</Button>}/>
</Card>
</Grid>
))}
{loading ? <Grid item xs={12} style={{textAlign: 'center'}}><CircularProgress/></Grid> : null}
</Grid>
</div>
<IconButton variant="contained" color="primary" style={{position: 'fixed', bottom: 10, right: 50}}
size="medium"
onClick={(e) => {
setShowConsole(!showConsole);
setAnchorEl(e.currentTarget);
setShowConfig(false);
}}
>
<BugReport/>
</IconButton>
<IconButton variant="contained" color="primary" style={{position: 'fixed', bottom: 10, right: 10}}
size="medium"
onClick={(e) => {
setAnchorEl(e.currentTarget);
setShowConfig(!showConfig)
setShowConsole(false);
}}
>
<Settings/>
</IconButton>
<Popper open={true} anchorEl={anchorEl} placement="top-end"
onClose={() => setShowConsole(false)} disablePortal={false}
modifiers={{
flip: {
enabled: true,
},
arrow: {
enabled: false,
// element: arrowRef,
},
}}>
<Card
style={showConsole ? {width: "90vw", height: "40vh", overflow: "scroll"} : {display: "none"}}>
<CardHeader title="Portfall Console" action={<IconButton style={{verticalAlign: 'middle'}}
onClick={() => setShowConsole(false)}><Close/></IconButton>}/>
<CardContent>
<Console/>
</CardContent>
</Card>
</Popper>
<Popper open={showConfig} anchorEl={anchorEl} placement="top-end"
onClose={() => setShowConfig(false)} disablePortal={false}
modifiers={{
flip: {
enabled: true,
},
arrow: {
enabled: false,
// element: arrowRef,
},
}}>
<Card square style={{width: "50vw"}}>
<CardHeader title="Portfall config" action={<IconButton style={{verticalAlign: 'middle'}}
onClick={() => setShowConfig(false)}><Close/></IconButton>}/>
<CardContent>
<Grid container spacing={3}>
<Grid item xs={8}>
<TextField defaultValue={configFilePath} fullWidth
disabled={false} label="Kubernetes config file" inputRef={confPathEl}/>
</Grid>
<Grid item xs={4} style={{alignSelf: "flex-end", textAlign: "end"}}>
<Button variant="contained" color="secondary" endIcon={<Folder/>}
onClick={() => {
window.backend.PortfallOS.OpenFile().then(f => {
if (f) {
confPathEl.current.value = f;
}
})
}}>Browse</Button>
</Grid>
{(availableContexts && currentContext) ?
<Grid item xs={8}>
<FormControl fullWidth>
<InputLabel>Config context</InputLabel>
<Select value={currentContext}
onChange={({target: {value}}) => setCurrentContext(value)}>
{availableContexts.map(c => <MenuItem value={c}>{c}</MenuItem>)}
</Select>
</FormControl>
</Grid> : null}
{configMessage ? (
<Grid item xs={12}>
<Alert severity={configMessage.severity} onClose={() => {
setConfigMessage(null)
}}>
{configMessage.message}
</Alert>
</Grid>) : null}
<Grid item xs={12} style={{textAlign: "center"}}>
<Button color="primary" variant="outlined" onClick={() => {
window.backend.Client.SetConfigPath(confPathEl.current.value, currentContext).then(([rv, ctx]) => {
if (rv === confPathEl.current.value) {
setConfigFilePath(rv);
refreshContext();
setConfigMessage({
severity: "success",
message: `Successfully set config to path ${rv} and context to ${ctx}`
})
} else {
// reset context
setCurrentContext(ctx);
setConfigMessage({
severity: "error",
message: `Failed to change config to path ${confPathEl.current.value}`
});
confPathEl.current.value = rv;
}
})
}}>Update Config</Button>
</Grid>
</Grid>
</CardContent>
</Card>
</Popper>
</div>
</ThemeProvider>
);
}
export default App;