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

Feature: add post-query options #878

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
34 changes: 34 additions & 0 deletions mwdb/web/src/commons/ui/ButtonDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconProp } from "@fortawesome/fontawesome-svg-core";

type Props = {
title?: string;
color?: string;
icon?: IconProp;
elements: JSX.Element[];
};

export function ButtonDropdown(props: Props) {
if (!props.elements.length) return <div />;
return (
<div className="nav-item dropdown">
<button
className={`btn btn-${props.color ? props.color : "info"} dropdown-toggle py-0`}
data-toggle="dropdown"
>
{props.icon ? (
<FontAwesomeIcon
className="navbar-icon"
icon={props.icon}
/>
) : (
[]
)}
{props.title}
</button>
<ul className="dropdown-menu button-menu" aria-labelledby="buttonDropdown">
{props.elements}
</ul>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useContext, useState } from "react";

import { Capability, ObjectData } from "@mwdb-web/types/types";
import { QueryResultContext } from "../common/QueryResultContext";
import { APIContext } from "@mwdb-web/commons/api";
import { ConfirmationModal } from "@mwdb-web/commons/ui";
import { AuthContext } from "@mwdb-web/commons/auth";
import { useViewAlert } from "@mwdb-web/commons/hooks";
import { ResultOptionItem } from "../common/ResultOptionItem";

export function AddTagAction() {
const api = useContext(APIContext);
const auth = useContext(AuthContext);
const { items } = useContext(QueryResultContext);

const { setAlert } = useViewAlert();

const [tag, setTag] = useState<string>("");
const [modalOpen, setIsModalOpen] = useState<boolean>(false);


function addTag() {
items.forEach(async (e: ObjectData) => {
await api.addObjectTag(e.id, tag)
.catch((err) => setAlert({
error: `Error adding tag to object ${e.id}: ${err}`
}));
});
setIsModalOpen(false);
}

return (
<ResultOptionItem
key={"tagOption"}
title={"Add Tag"}
action={() => setIsModalOpen(true)}
authenticated={() => auth.hasCapability(Capability.addingTags)}
>
<ConfirmationModal
isOpen={modalOpen}
confirmText="Ok"
cancelText="Cancel"
message="Please enter a tag to add"
onRequestClose={() => setIsModalOpen(false)}
onCancel={() => setIsModalOpen(false)}
onConfirm={addTag}
>
<input
className="form-control small"
onChange={(e) => setTag(e.target.value)}
/>
</ConfirmationModal>
</ResultOptionItem>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useContext, useEffect, useState } from "react";

import { QueryResultContext } from "../common/QueryResultContext";
import { ResultOptionItem } from "../common/ResultOptionItem";
import { ObjectData } from "@mwdb-web/types/types";


export function QueryResultHashesAction() {
const { items } = useContext(QueryResultContext);
const [url, setUrl] = useState<string>("");

function generateName() {
return `hashes_${new Date().toJSON().slice(0, 19)}`;
}

async function generateUrl() {
const hashes = items.map((item: ObjectData) => item.sha256);
const data = new Blob([JSON.stringify(hashes, null, '\t')], { type: 'application/json' })
setUrl(window.URL.createObjectURL(data));
}

useEffect(() => {
generateUrl();
}, [])

return (
<ResultOptionItem
url={url}
key={"hashesOption"}
title={"Download Hashes"}
download={generateName}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useContext, useEffect, useState } from "react";

import { QueryResultContext } from "../common/QueryResultContext";
import { ResultOptionItem } from "../common/ResultOptionItem";


export function QueryResultJsonAction() {
const { items } = useContext(QueryResultContext);
const [url, setUrl] = useState<string>("");

function generateName() {
return `file_data_${new Date().toJSON().slice(0, 19)}`;
}

async function generateUrl() {
const data = new Blob([JSON.stringify(items, null, '\t')], { type: 'application/json' })
setUrl(window.URL.createObjectURL(data));
}

useEffect(() => {
generateUrl();
}, [])

return (
<ResultOptionItem
url={url}
key={"jsonOption"}
title={"Download JSON"}
download={generateName}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useContext, useState } from "react";

import { Capability, ObjectData } from "@mwdb-web/types/types";
import { QueryResultContext } from "../common/QueryResultContext";
import { APIContext } from "@mwdb-web/commons/api";
import { ConfirmationModal } from "@mwdb-web/commons/ui";
import { AuthContext } from "@mwdb-web/commons/auth";
import { useViewAlert } from "@mwdb-web/commons/hooks";
import { ResultOptionItem } from "../common/ResultOptionItem";

export function KartonReanalyzeAction() {
const api = useContext(APIContext);
const auth = useContext(AuthContext);
const { items } = useContext(QueryResultContext);

const { setAlert } = useViewAlert();

const [modalOpen, setIsModalOpen] = useState<boolean>(false);


function kartonReanalyze() {
items.forEach(async (e: ObjectData) => {
await api.resubmitKartonAnalysis(e.id)
.catch((err) => setAlert({
error: `Error submitting reanalysis for object ${e.id}: ${err}`
}));
});
setIsModalOpen(false);
}

return (
<ResultOptionItem
key={"kartonReanalyzeOption"}
title={"Karton Reanalysis"}
action={() => setIsModalOpen(true)}
authenticated={() => auth.hasCapability(Capability.kartonReanalyze)}
>
<ConfirmationModal
isOpen={modalOpen}
message="Are you sure you want to reanalyze?"
onRequestClose={() => setIsModalOpen(false)}
onCancel={() => setIsModalOpen(false)}
onConfirm={kartonReanalyze}
/>
</ResultOptionItem>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useContext, useState } from "react";

import { Capability, ObjectData } from "@mwdb-web/types/types";
import { QueryResultContext } from "../common/QueryResultContext";
import { APIContext } from "@mwdb-web/commons/api";
import { ConfirmationModal } from "@mwdb-web/commons/ui";
import { AuthContext } from "@mwdb-web/commons/auth";
import { useViewAlert } from "@mwdb-web/commons/hooks";
import { ResultOptionItem } from "../common/ResultOptionItem";

export function RemoveTagAction() {
const api = useContext(APIContext);
const auth = useContext(AuthContext);
const { items } = useContext(QueryResultContext);

const { setAlert } = useViewAlert();

const [tag, setTag] = useState<string>("");
const [modalOpen, setIsModalOpen] = useState<boolean>(false);


function addTag() {
items.forEach(async (e: ObjectData) => {
await api.removeObjectTag(e.id, tag)
.catch((err) => setAlert({
error: `Error removing tag from object ${e.id}: ${err}`
}));
});
setIsModalOpen(false);
}

return (
<ResultOptionItem
key={"removeTagOption"}
title={"Remove Tag"}
action={() => setIsModalOpen(true)}
authenticated={() => auth.hasCapability(Capability.addingTags)}
>
<ConfirmationModal
isOpen={modalOpen}
confirmText="Ok"
cancelText="Cancel"
message="Please enter a tag to remove"
onRequestClose={() => setIsModalOpen(false)}
onCancel={() => setIsModalOpen(false)}
onConfirm={addTag}
>
<input
className="form-control small"
onChange={(e) => setTag(e.target.value)}
/>
</ConfirmationModal>
</ResultOptionItem>
);
}
38 changes: 38 additions & 0 deletions mwdb/web/src/components/RecentView/Views/QueryResultOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { faExclamationCircle, faMagnifyingGlass, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { ButtonDropdown } from "@mwdb-web/commons/ui/ButtonDropdown";
import { ObjectType } from "@mwdb-web/types/types";
import { QueryResultHashesAction } from "../Actions/QueryResultHashesAction";
import { useContext } from "react";
import { QueryResultContext } from "../common/QueryResultContext";
import { QueryResultJsonAction } from "../Actions/QueryResultJsonAction";
import { AddTagAction } from "../Actions/QueryResultAddTagAction";
import { RemoveTagAction } from "../Actions/QueryResultRemoveTagAction";
import { KartonReanalyzeAction } from "../Actions/QueryResultKartonReanalyzeAction";

type Props = {
type: ObjectType,
query: string,
elements?: JSX.Element[],
};

export function QueryResultOptions(props: Props) {
const { items } = useContext(QueryResultContext);
return (
<div>
{props.query && items && items.length > 0 &&
<div className="quick-query-bar">
<ButtonDropdown
title="Result Options"
icon={faMagnifyingGlass}
elements={[
<QueryResultHashesAction/>,
<QueryResultJsonAction/>,
<KartonReanalyzeAction/>,
<AddTagAction/>,
<RemoveTagAction/>,
]}
/>
</div>}
</div>
);
}
37 changes: 25 additions & 12 deletions mwdb/web/src/components/RecentView/Views/RecentView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { QuickQuery } from "../common/QuickQuery";
import { ObjectType } from "@mwdb-web/types/types";
import { AxiosError } from "axios";
import { isEmpty } from "lodash";
import { Extendable } from "@mwdb-web/commons/plugins";
import { QueryResultOptions } from "./QueryResultOptions";
import { QueryResultContextProvider } from "../common/QueryResultContext";

type Props = {
type: ObjectType;
Expand Down Expand Up @@ -218,8 +221,8 @@ export function RecentView(props: Props) {
className="btn-group"
data-toggle="tooltip"
title={`Turn ${
countingEnabled ? "off" : "on"
} results counting`}
countingEnabled ? "off" : "on"
} results counting`}
>
<input
type="button"
Expand Down Expand Up @@ -264,16 +267,26 @@ export function RecentView(props: Props) {
/>
</div>
</form>
<RecentViewList
query={submittedQuery}
type={props.type}
rowComponent={props.rowComponent}
headerComponent={props.headerComponent}
locked={isLocked}
disallowEmpty={props.disallowEmpty ?? false}
setQueryError={setQueryError}
addToQuery={addToQuery}
/>
<QueryResultContextProvider>
<div className="query-options">
<Extendable ident="queryPostResult">
<QueryResultOptions
query={submittedQuery}
type={props.type}
/>
</Extendable>
</div>
<RecentViewList
query={submittedQuery}
type={props.type}
rowComponent={props.rowComponent}
headerComponent={props.headerComponent}
locked={isLocked}
disallowEmpty={props.disallowEmpty ?? false}
setQueryError={setQueryError}
addToQuery={addToQuery}
/>
</QueryResultContextProvider>
</div>
</View>
);
Expand Down
6 changes: 6 additions & 0 deletions mwdb/web/src/components/RecentView/Views/RecentViewList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ObjectType,
} from "@mwdb-web/types/types";
import { AxiosError } from "axios";
import { QueryResultContext } from "../common/QueryResultContext";

type Elements = ObjectData[] | BlobData[] | ConfigData[];

Expand Down Expand Up @@ -83,6 +84,7 @@ type Props = {

export function RecentViewList(props: Props) {
const api = useContext(APIContext);
const { setItems } = useContext(QueryResultContext);
const [listState, listDispatch] = useReducer(listStateReducer, {
pageToLoad: 0,
loadedPages: 0,
Expand All @@ -103,6 +105,10 @@ export function RecentViewList(props: Props) {
}
}, [props.query, props.disallowEmpty, api.remote]);

useEffect(() => {
setItems(listState.elements);
}, [listState.elements]);

// Load page on request (pageToLoad != loadedPages)
useEffect(() => {
let cancelled = false;
Expand Down
Loading
Loading