frontend/src/pages/Terminals.tsx
2025-10-21 10:44:46 -06:00

459 lines
13 KiB
TypeScript

import {
Tab,
Tabs,
Button,
Theme,
useTheme,
Menu,
MenuItem,
ListItemIcon,
ListItemText,
DialogTitle,
DialogContent,
DialogActions
} from "@mui/material";
import { Terminal } from "models";
import { useTerminalAPI, useGlobalState } from "providers";
import React, { useCallback, useEffect, useState } from "react";
import PageContainer from "./PageContainer";
import AddIcon from "@mui/icons-material/Add";
import TerminalSettings from "terminal/TerminalSettings";
import { Delete, MoreVert, ExitToApp as RemoveSelfIcon } from "@mui/icons-material";
import EditIcon from "@mui/icons-material/Edit";
import { blue, green, red } from "@mui/material/colors";
import ResponsiveDialog from "common/ResponsiveDialog";
import { useSnackbar, useUserAPI } from "hooks";
import GateList from "gate/GateList";
import { Scope } from "models";
import { pond } from "protobuf-ts/pond";
import ObjectUsers from "user/ObjectUsers";
import ObjectTeams from "teams/ObjectTeams";
import ObjectUsersIcon from "@mui/icons-material/AccountCircle";
import ObjectTeamsIcon from "@mui/icons-material/SupervisedUserCircle";
import ShareObject from "user/ShareObject";
import RemoveSelfFromObject from "user/RemoveSelfFromObject";
import { makeStyles } from "@mui/styles";
const parentTab = {
"&": {
margin: "0px",
marginTop: "4px",
marginLeft: "4px",
animationDuration: "10s",
background: "rgba(150, 150, 150, 0)",
borderRadius: "-5px",
borderTopLeftRadius: "6px",
borderTopRightRadius: "6px"
},
"&:hover": {
background: "linear-gradient(rgba(150, 150, 150, 0.2), rgba(150, 150, 150, 0))"
}
};
const useStyles = makeStyles((theme: Theme) => ({
tab: {
...parentTab,
minWidth: theme.spacing(25),
left: 0,
height: "100%",
background: theme.palette.background.paper
},
smallTab: {
...parentTab,
width: theme.spacing(8),
minWidth: theme.spacing(8),
background: theme.palette.background.paper
},
selectedTab: {
borderRadius: "-5px",
borderTopLeftRadius: "6px",
borderTopRightRadius: "6px",
background: theme.palette.background.default,
"&:hover": {
background:
"linear-gradient(rgba(150, 150, 150, 0.3)," + theme.palette.background.default + " 80%)"
}
},
tabText: {
position: "absolute",
left: theme.spacing(2),
color: "transparent",
textAlign: "left",
width: "80%",
backgroundImage: "linear-gradient(to right, white 70%, rgba(200, 200, 200, 0) 87.5%)",
backgroundClip: "text",
WebkitBackgroundClip: "text",
overflowX: "hidden",
whiteSpace: "nowrap",
top: 12
},
icon: {
display: "flex",
position: "absolute",
right: 0,
top: 6,
padding: 6,
marginRight: "2px",
width: 36,
height: 36,
borderRadius: "18px",
background: "rgba(0,0,0,0)",
"&:hover": {
background:
"radial-gradient(closest-side, rgba(150, 150, 150, 0.5) 50%, rgba(150, 150, 150, 0.5))"
}
},
green: {
marginRight: 0,
paddingRight: 0,
color: green["500"],
"&:hover": {
color: green["600"]
}
},
red: {
marginRight: 0,
paddingRight: 0,
color: red["500"],
"&:hover": {
color: red["600"]
}
},
blueIcon: {
color: blue["500"],
"&:hover": {
color: blue["600"]
}
},
addGate: {
position: "absolute",
bottom: 20,
right: 20,
backgroundColor: theme.palette.primary.main
}
}));
interface Props {
terminal?: Terminal;
}
export default function Terminals(props: Props) {
const terminalAPI = useTerminalAPI();
const [value, setValue] = useState(0);
const [terminals, setTerminals] = useState<Terminal[]>([]);
const classes = useStyles();
const theme = useTheme();
const [addTerminal, setAddTerminal] = useState(false);
const [menuAnchorEl, setMenuAnchorEl] = useState<Element | null>(null);
const [currentTerminal, setCurrentTerminal] = useState<Terminal>();
const [removeTerminal, setRemoveTerminal] = useState(false);
const { openSnack } = useSnackbar();
const [{ user, as }] = useGlobalState();
const userAPI = useUserAPI();
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
const [userSharing, setUserSharing] = useState(false);
const [teamSharing, setTeamSharing] = useState(false);
const [baseShareDialog, setBaseShareDialog] = useState(false);
const [removeSelfDialog, setRemoveSelfDialog] = useState(false);
const [loading, setLoading] = useState(false)
useEffect(() => {
if (currentTerminal) {
let key = currentTerminal?.key;
let kind = "terminal";
if (as) {
key = as;
kind = "team";
}
userAPI.getUser(user.id(), { key: key, kind: kind } as Scope).then(resp => {
setPermissions(resp.permissions);
});
}
}, [as, userAPI, user, currentTerminal]);
useEffect(() => {
if (props.terminal) {
setCurrentTerminal(props.terminal);
}
}, [props.terminal]);
const load = useCallback(() => {
if(loading) return
setLoading(true)
terminalAPI.listTerminals(200, 0, "asc", "name", as).then(resp => {
if(resp.data.terminals){
setTerminals(resp.data.terminals.map(a => Terminal.any(a)));
}
}).catch(err => {
console.log("There was a problem loading Terminals")
}).finally(() => {
setLoading(false)
})
}, [terminalAPI, openSnack, as]); //eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
load();
}, [load]);
const remove = () => {
if (currentTerminal) {
terminalAPI
.removeTerminal(currentTerminal.key, as)
.then(resp => {
openSnack("Terminal Deleted");
let a = terminals;
a.splice(terminals.indexOf(currentTerminal), 1);
setTerminals([...a]);
})
.catch(err => {
openSnack("Failed to delete terminal");
})
.finally(() => {
setRemoveTerminal(false);
});
}
};
const terminalMenu = () => {
if (currentTerminal === undefined) return;
return (
<Menu
id="terminalMenu"
anchorEl={menuAnchorEl ? menuAnchorEl : null}
open={menuAnchorEl !== null}
onClose={() => {
setMenuAnchorEl(null);
}}>
{permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<MenuItem
onClick={() => {
setAddTerminal(true);
setMenuAnchorEl(null);
}}
aria-label="Edit Terminal"
dense>
<ListItemIcon>
<EditIcon className={classes.green} />
</ListItemIcon>
<ListItemText secondary="Edit Terminal" />
</MenuItem>
)}
{permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<MenuItem
onClick={() => {
setRemoveTerminal(true);
setMenuAnchorEl(null);
}}
aria-label="Remove Terminal"
dense>
<ListItemIcon>
<Delete className={classes.red} />
</ListItemIcon>
<ListItemText secondary="Delete Terminal" />
</MenuItem>
)}
{permissions.includes(pond.Permission.PERMISSION_USERS) && (
<MenuItem
dense
onClick={() => {
setUserSharing(true);
setMenuAnchorEl(null);
}}>
<ListItemIcon>
<ObjectUsersIcon />
</ListItemIcon>
<ListItemText primary="Users" />
</MenuItem>
)}
{permissions.includes(pond.Permission.PERMISSION_USERS) && (
<MenuItem
dense
onClick={() => {
setTeamSharing(true);
setMenuAnchorEl(null);
}}>
<ListItemIcon>
<ObjectTeamsIcon />
</ListItemIcon>
<ListItemText primary="Teams" />
</MenuItem>
)}
<MenuItem
dense
onClick={() => {
setRemoveSelfDialog(true);
setMenuAnchorEl(null);
}}>
<ListItemIcon>
<RemoveSelfIcon className={classes.red} />
</ListItemIcon>
<ListItemText primary="Leave" />
</MenuItem>
</Menu>
);
};
const changeTabs = (newValue: number) => {
//TODO: change this to just change the selected tab
if (newValue <= terminals.length) {
setValue(newValue);
if (terminals[newValue - 1]) {
setCurrentTerminal(terminals[newValue -1])
} else {
setCurrentTerminal(undefined)
}
}
};
const gateDisplay = () => {
return (
<GateList
currentTerminal={currentTerminal?.key}
terminals={terminals}
useMobile={props.terminal !== undefined}
/>
);
};
//could update the tabs in the same way as the bin yard tabs
const terminalTabs = () => {
return (
<React.Fragment>
{/* TODO: convert this to use DraggableTabs */}
<Tabs
variant="scrollable"
scrollButtons="auto"
value={value}
onChange={(_, v) => changeTabs(v)}
TabIndicatorProps={{ style: { background: "rgba(0,0,0,0)" } }}>
<Tab
key={"all"}
label="All"
disableTouchRipple
classes={{ root: classes.smallTab, selected: classes.selectedTab }}
style={{ marginLeft: theme.spacing(1) }}
/>
{terminals.map((a, index) => (
<Tab
classes={{ root: classes.tab, selected: classes.selectedTab }}
disableTouchRipple
key={a.key}
label={
<span>
<div className={classes.tabText}>{a.name}</div>
<MoreVert
className={classes.icon}
onClick={event => {
event.stopPropagation()
let target = event.currentTarget;
setMenuAnchorEl(target);
setCurrentTerminal(terminals[index]);
}}
/>
</span>
}
/>
))}
<Tab
classes={{ root: classes.smallTab, selected: classes.selectedTab }}
label={terminals.length === 0 ? "Add Terminal" : <AddIcon />}
onClick={() => {
setCurrentTerminal(undefined);
setAddTerminal(true);
}}
disableTouchRipple={true}
/>
</Tabs>
</React.Fragment>
);
};
const removeDialog = () => {
return (
<ResponsiveDialog
open={removeTerminal}
onClose={() => {
setRemoveTerminal(false);
}}>
<DialogTitle>Delete Terminal</DialogTitle>
<DialogContent>Are you sure you wish to delete this Terminal?</DialogContent>
<DialogActions>
<Button>Cancel</Button>
<Button onClick={remove}>Confirm</Button>
</DialogActions>
</ResponsiveDialog>
);
};
const sharingDialogs = () => {
if (!currentTerminal) return;
return (
<React.Fragment>
<ShareObject
scope={{ kind: "terminal", key: currentTerminal.key } as Scope}
label={currentTerminal.name}
permissions={permissions}
isDialogOpen={baseShareDialog}
closeDialogCallback={() => {
setBaseShareDialog(false);
}}
/>
<ObjectUsers
scope={{ kind: "terminal", key: currentTerminal.key } as Scope}
label={currentTerminal.name}
permissions={permissions}
isDialogOpen={userSharing}
closeDialogCallback={() => setUserSharing(false)}
refreshCallback={() => {}}
/>
<ObjectTeams
scope={{ kind: "terminal", key: currentTerminal.key } as Scope}
label={currentTerminal.name}
permissions={permissions}
isDialogOpen={teamSharing}
closeDialogCallback={() => setTeamSharing(false)}
refreshCallback={() => {
return true;
}}
/>
<RemoveSelfFromObject
scope={{ kind: "terminal", key: currentTerminal.key } as Scope}
label={currentTerminal.name}
isDialogOpen={removeSelfDialog}
closeDialogCallback={removed => {
if (removed) {
let a = terminals;
a.splice(terminals.indexOf(currentTerminal), 1);
setTerminals([...a]);
}
setRemoveSelfDialog(false);
}}
/>
</React.Fragment>
);
};
return (
<PageContainer>
<TerminalSettings
open={addTerminal}
closeDialog={newTerminal => {
if (newTerminal) {
let a = terminals;
a.push(newTerminal);
setTerminals([...a]);
}
setAddTerminal(false);
}}
terminal={currentTerminal}
/>
{!props.terminal && terminalTabs()}
{terminalMenu()}
{removeDialog()}
{sharingDialogs()}
{gateDisplay()}
</PageContainer>
);
}