Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save ZTF alerts as sources by objectId #44

Merged
merged 24 commits into from
Sep 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
483 changes: 431 additions & 52 deletions extensions/skyportal/skyportal/handlers/api/alert.py

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions extensions/skyportal/static/js/components/Filter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const Filter = () => {
useEffect(() => {
const fetchFilterVersion = async () => {
const data = await dispatch(filterVersionActions.fetchFilterVersion(fid));
if ((data.status === "error") && !(data.message.includes("not found"))) {
if (data.status === "error" && !data.message.includes("not found")) {
setFilterVersionLoadError(data.message);
if (filterVersionLoadError.length > 1) {
dispatch(showNotification(filterVersionLoadError, "error"));
Expand All @@ -127,7 +127,7 @@ const Filter = () => {
if (loadedId !== fid) {
fetchFilterVersion();
}
}, [fid, loadedId, dispatch]);
}, [fid, loadedId, dispatch, filterVersionLoadError]);

const group_id = useSelector((state) => state.filter.group_id);

Expand All @@ -142,7 +142,7 @@ const Filter = () => {
}
};
if (group_id) fetchGroup();
}, [group_id, dispatch]);
}, [group_id, dispatch, groupLoadError]);

const filter = useSelector((state) => state.filter);
const filter_v = useSelector((state) => state.filter_v);
Expand Down
221 changes: 221 additions & 0 deletions extensions/skyportal/static/js/components/SaveAlertButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import React, { useState, useEffect, useRef } from "react";
import PropTypes from "prop-types";
import { useDispatch } from "react-redux";
import Dialog from "@material-ui/core/Dialog";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import Checkbox from "@material-ui/core/Checkbox";
import Button from "@material-ui/core/Button";
import ButtonGroup from "@material-ui/core/ButtonGroup";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
import ClickAwayListener from "@material-ui/core/ClickAwayListener";
import Grow from "@material-ui/core/Grow";
import Paper from "@material-ui/core/Paper";
import Popper from "@material-ui/core/Popper";
import MenuItem from "@material-ui/core/MenuItem";
import MenuList from "@material-ui/core/MenuList";
import { useForm, Controller } from "react-hook-form";

import * as alertActions from "../ducks/alert";
import * as sourceActions from "../ducks/source";
import FormValidationError from "./FormValidationError";

const SaveAlertButton = ({ alert, userGroups }) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when a user tries to save an object with only PID=3 alerts to the sitewide group?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point, addressed here. It will now fail.

const [isSubmitting, setIsSubmitting] = useState(false);
// Dialog logic:

const dispatch = useDispatch();
const [dialogOpen, setDialogOpen] = useState(false);

const { handleSubmit, errors, reset, control, getValues } = useForm();

useEffect(() => {
reset({
group_ids: []
});
}, [reset, userGroups, alert]);

const handleClickOpenDialog = () => {
setDialogOpen(true);
};

const handleCloseDialog = () => {
setDialogOpen(false);
};

const validateGroups = () => {
const formState = getValues({ nest: true });
return formState.group_ids.filter((value) => Boolean(value)).length >= 1;
};

const onSubmitGroupSelectSave = async (data) => {
setIsSubmitting(true);
data.id = alert.id;
const groupIDs = userGroups.map((g) => g.id);
const selectedGroupIDs = groupIDs.filter((ID, idx) => data.group_ids[idx]);

data.payload = {candid: alert.candid, group_ids: selectedGroupIDs};

const result = await dispatch(alertActions.saveAlertAsSource(data));
if (result.status === "error") {
setIsSubmitting(false);
} else {
setDialogOpen(false);
reset();
await dispatch(sourceActions.fetchSource(alert.id));
}
};

// Split button logic (largely copied from
// https://material-ui.com/components/button-group/#split-button):

const options = ["Select groups & save as a source"];
// const options = ["Select groups & save as a source", "Select filters & save as a candidate"];

const [splitButtonMenuOpen, setSplitButtonMenuOpen] = useState(false);
const anchorRef = useRef(null);
const [selectedIndex, setSelectedIndex] = useState(0);

const handleClickMainButton = async () => {
if (selectedIndex === 0) {
handleClickOpenDialog();
}
};

const handleMenuItemClick = (event, index) => {
setSelectedIndex(index);
setSplitButtonMenuOpen(false);
};

const handleToggleSplitButtonMenu = () => {
setSplitButtonMenuOpen((prevOpen) => !prevOpen);
};

const handleCloseSplitButtonMenu = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setSplitButtonMenuOpen(false);
};

return (
<div>
<ButtonGroup
variant="contained"
ref={anchorRef}
aria-label="split button"
>
<Button
onClick={handleClickMainButton}
name={`initialSaveAlertButton${alert.id}`}
data-testid={`saveAlertButton_${alert.id}`}
disabled={isSubmitting}
>
{options[selectedIndex]}
</Button>
<Button
size="small"
aria-controls={splitButtonMenuOpen ? "split-button-menu" : undefined}
aria-expanded={splitButtonMenuOpen ? "true" : undefined}
aria-label="Save as Source"
aria-haspopup="menu"
name={`saveAlertButtonDropDownArrow${alert.id}`}
onClick={handleToggleSplitButtonMenu}
>
<ArrowDropDownIcon />
</Button>
</ButtonGroup>
<Popper
open={splitButtonMenuOpen}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
style={{ zIndex: 1000 }}
>
{({ TransitionProps, placement }) => (
<Grow
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...TransitionProps}
style={{
transformOrigin:
placement === "bottom" ? "center top" : "center bottom",
}}
>
<Paper>
<ClickAwayListener onClickAway={handleCloseSplitButtonMenu}>
<MenuList id="split-button-menu">
{options.map((option, index) => (
<MenuItem
key={option}
name={`buttonMenuOption${alert.id}_${option}`}
selected={index === selectedIndex}
onClick={(event) => handleMenuItemClick(event, index)}
>
{option}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>

<Dialog
open={dialogOpen}
onClose={handleCloseDialog}
style={{ position: "fixed" }}
>
<DialogTitle>Select one or more groups:</DialogTitle>
<DialogContent>
<form onSubmit={handleSubmit(onSubmitGroupSelectSave)}>
{errors.group_ids && (
<FormValidationError message="Select at least one group." />
)}
{userGroups.map((userGroup, idx) => (
<FormControlLabel
key={userGroup.id}
control={
<Controller
as={Checkbox}
name={`group_ids[${idx}]`}
control={control}
rules={{ validate: validateGroups }}
defaultValue={false}
/>
}
label={userGroup.name}
/>
))}
<br />
<div style={{ textAlign: "center" }}>
<Button
variant="contained"
type="submit"
name={`finalSaveAlertButton${alert.id}`}
>
Save
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
);
};
SaveAlertButton.propTypes = {
alert: PropTypes.shape({
id: PropTypes.string,
group_ids: PropTypes.arrayOf(PropTypes.number),
}).isRequired,
userGroups: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string,
})
).isRequired,
};

export default SaveAlertButton;
Loading