import React, { useEffect } from 'react'; import { opsgenieApiRef } from '../../api'; import { useAsync } from "react-use"; import Alert from "@material-ui/lab/Alert"; import { Card, CardContent, CardHeader, createStyles, TextField, InputAdornment, List, ListItem, ListItemIcon, ListItemText, makeStyles, Tooltip } from '@material-ui/core'; import PersonIcon from '@material-ui/icons/Person'; import { Schedule } from '../../types'; import { Pagination } from '@material-ui/lab'; import SearchIcon from '@material-ui/icons/Search'; import { useApi } from '@backstage/core-plugin-api'; import { Progress, ItemCardGrid, StatusOK, StatusAborted } from '@backstage/core-components'; const useStyles = makeStyles((theme) => createStyles({ pagination: { marginTop: theme.spacing(2), }, search: { marginBottom: theme.spacing(2), }, onCallItemGrid: { gridTemplateColumns: 'repeat(auto-fill, minmax(32em, 1fr))', } }), ); const useDebounce = (value: string, delay: number) => { const [debouncedValue, setDebouncedValue] = React.useState(value); useEffect( () => { // Update debounced value after delay const handler = setTimeout(() => { setDebouncedValue(value); }, delay); // Cancel the timeout if value changes (also on delay change or unmount) // This is how we prevent debounced value from updating if value is changed ... // .. within the delay period. Timeout gets cleared and restarted. return () => { clearTimeout(handler); }; }, [value, delay] // Only re-call effect if value or delay changes ); return debouncedValue; } export const OnCallForScheduleList = ({ schedule }: { schedule: Schedule }) => { const opsgenieApi = useApi(opsgenieApiRef); const { value, loading, error } = useAsync(async () => await opsgenieApi.getOnCall(schedule.id)); if (loading) { return <Progress />; } else if (error) { return ( <Alert data-testid="error-message" severity="error"> {error.message} </Alert> ); } return ( <List> {value!.map((responder, i) => ( <ListItem key={i} button component="a" href={opsgenieApi.getUserDetailsURL(responder.id)}> <ListItemIcon> <PersonIcon /> </ListItemIcon> <ListItemText primary={responder.name} /> </ListItem> ))} {value!.length === 0 && <ListItem><ListItemText primary="⚠️ No one on-call." /></ListItem>} </List> ); }; export const OnCallForScheduleCard = ({ schedule }: { schedule: Schedule }) => { const title = ( <div style={{ display: "flex" }}> <Tooltip title={schedule.enabled ? 'Enabled' : 'Disabled'}> <div>{schedule.enabled ? <StatusOK /> : <StatusAborted />}</div> </Tooltip> {schedule.ownerTeam.name} </div> ); return ( <Card> <CardHeader title={title} titleTypographyProps={{ variant: 'h6' }} /> <CardContent> <OnCallForScheduleList schedule={schedule} /> </CardContent> </Card> ); }; const SchedulesGrid = ({ schedules, cardsPerPage }: { schedules: Schedule[], cardsPerPage: number }) => { const classes = useStyles(); const [results, setResults] = React.useState(schedules); const [search, setSearch] = React.useState(""); const [page, setPage] = React.useState(1); const [offset, setOffset] = React.useState(0); const handleChange = (_: React.ChangeEvent<unknown>, value: number) => { setOffset((value - 1) * cardsPerPage); setPage(value); }; const debouncedSearch = useDebounce(search, 300); // debounced search useEffect( () => { if (!debouncedSearch) { setResults(schedules); return; } const filtered = schedules.filter(schedule => { return schedule.name.toLowerCase().includes(debouncedSearch.toLowerCase()); }); setResults(filtered); }, [debouncedSearch, schedules] ); return ( <div> <TextField fullWidth variant="outlined" className={classes.search} placeholder="Team…" InputProps={{ startAdornment: ( <InputAdornment position="start"> <SearchIcon /> </InputAdornment> ) }} onChange={e => setSearch(e.target.value)} /> <ItemCardGrid classes={{ root: classes.onCallItemGrid }}> {results.filter((_, i) => i >= offset && i < offset + cardsPerPage).map(schedule => <OnCallForScheduleCard key={schedule.id} schedule={schedule} />)} </ItemCardGrid> <Pagination className={classes.pagination} count={Math.ceil(results.length / cardsPerPage)} page={page} onChange={handleChange} showFirstButton showLastButton /> </div> ); }; export type OnCallListProps = { cardsPerPage?: number; }; export const OnCallList = ({ cardsPerPage }: OnCallListProps) => { const opsgenieApi = useApi(opsgenieApiRef); const { value, loading, error } = useAsync(async () => await opsgenieApi.getSchedules()); if (loading) { return <Progress />; } else if (error) { return ( <Alert data-testid="error-message" severity="error"> {error.message} </Alert> ); } return <SchedulesGrid schedules={value!.filter(schedule => schedule.enabled)} cardsPerPage={cardsPerPage || 6} />; };