Files
its-a-feature-Mythic/MythicReactUI/src/components/pages/Search/SearchTabCustomBrowsers.js
T
2025-12-10 08:20:43 -06:00

417 lines
17 KiB
JavaScript

import {MythicTabPanel, MythicSearchTabLabel} from '../../MythicComponents/MythicTabPanel';
import React from 'react';
import MythicTextField from '../../MythicComponents/MythicTextField';
import FolderIcon from '@mui/icons-material/Folder';
import Grid from '@mui/material/Grid';
import SearchIcon from '@mui/icons-material/Search';
import Tooltip from '@mui/material/Tooltip';
import {useTheme} from '@mui/material/styles';
import IconButton from '@mui/material/IconButton';
import { gql, useQuery} from '@apollo/client';
import { snackActions } from '../../utilities/Snackbar';
import Pagination from '@mui/material/Pagination';
import { Typography } from '@mui/material';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
import {CustomBrowserTable} from "./CustomBrowserTable";
const mythictreeFragment = gql`
fragment mythictreeSearchData on mythictree{
comment
deleted
full_path_text
host
id
name_text
metadata
task {
display_id
}
callback {
mythictree_groups
id
display_id
}
tags {
tagtype {
name
color
id
}
id
}
}
`;
const nameSearch = gql`
${mythictreeFragment}
query nameCustomBrowserQuery($name: String!, $host: String!, $offset: Int!, $fetchLimit: Int!, $tree_type: String!) {
mythictree_aggregate(distinct_on: id, where: {full_path_text: {_ilike: $name}, host: {_ilike: $host}, tree_type: {_eq: $tree_type}}) {
aggregate {
count
}
}
mythictree(limit: $fetchLimit, distinct_on: id, offset: $offset, order_by: {id: desc}, where: {host: {_ilike: $host}, full_path_text: {_ilike: $name}, tree_type: {_eq: $tree_type}}) {
...mythictreeSearchData
}
}
`;
const metadataSearch = gql`
${mythictreeFragment}
query metadataCustomBrowserQuery($search: String!, $host: String!, $offset: Int!, $fetchLimit: Int!, $tree_type: String!) {
mythictree_aggregate(distinct_on: id, where: {metadata: {_cast: {String: {_ilike: $search}}}, host: {_ilike: $host}, tree_type: {_eq: $tree_type}}) {
aggregate {
count
}
}
mythictree(limit: $fetchLimit, distinct_on: id, offset: $offset, order_by: {id: desc}, where: {host: {_ilike: $host}, metadata: {_cast: {String: {_ilike: $search}}}, tree_type: {_eq: $tree_type}}) {
...mythictreeSearchData
}
}
`;
const commentSearch = gql`
${mythictreeFragment}
query nameCustomBrowserQuery($comment: String!, $host: String!, $offset: Int!, $fetchLimit: Int!, $tree_type: String!) {
mythictree_aggregate(distinct_on: id, where: {comment: {_ilike: $comment}, host: {_ilike: $host}, tree_type: {_eq: $tree_type}}) {
aggregate {
count
}
}
mythictree(limit: $fetchLimit, distinct_on: id, offset: $offset, order_by: {id: desc}, where: {host: {_ilike: $host}, comment: {_ilike: $comment}, tree_type: {_eq: $tree_type}}) {
...mythictreeSearchData
}
}
`;
const tagSearch = gql`
${mythictreeFragment}
query tagCustomBrowserQuery($tag: String!, $host: String!, $offset: Int!, $fetchLimit: Int!, $tree_type: String!) {
tag_aggregate(distinct_on: mythictree_id, where: {mythictree_id: {_is_null: false}, _or: [{data: {_cast: {String: {_ilike: $tag}}}}, {tagtype: {name: {_ilike: $tag}}}], mythictree: {host: {_ilike: $host}, tree_type: {_eq: $tree_type}}}) {
aggregate {
count
}
}
tag(limit: $fetchLimit, distinct_on: mythictree_id, offset: $offset, order_by: {mythictree_id: desc}, where: {mythictree_id: {_is_null: false}, _or: [{data: {_cast: {String: {_ilike: $tag}}}}, {tagtype: {name: {_ilike: $tag}}}], mythictree: {host: {_ilike: $host}, tree_type: {_eq: $tree_type}}}) {
mythictree {
...mythictreeSearchData
}
}
}
`;
const customBrowsers = gql`
query getCustomBrowsers {
custombrowser(where: {deleted: {_eq: false}}, order_by: {name: asc}){
name
columns
}
}
`;
const fetchLimit = 100;
export function SearchTabCustomBrowserLabel(props){
return (
<MythicSearchTabLabel label={"Browsers"} iconComponent={<FolderIcon />} {...props}/>
)
}
const SearchTabCustomBrowserSearchPanel = (props) => {
const theme = useTheme();
const [browserOptions, setBrowserOptions] = React.useState([]);
const [selectedBrowser, setSelectedBrowser] = React.useState("");
const [search, setSearch] = React.useState("");
const [searchHost, setSearchHost] = React.useState("");
const [searchField, setSearchField] = React.useState("Name");
const [searchFieldOptions, setSearchFieldOptions] = React.useState(["Name", "Tags", "Comment", "Metadata"]);
const handleBrowserChange = (event) => {
setSelectedBrowser(event.target.value);
for(let i = 0; i < browserOptions.length; i++) {
if(browserOptions[i].name === event.target.value){
props.onChangeBrowserField(browserOptions[i]);
return;
}
}
}
const handleSearchFieldChange = (event) => {
setSearchField(event.target.value);
props.onChangeSearchField(event.target.value);
props.changeSearchParam("searchField", event.target.value);
}
const handleSearchHostValueChange = (name, value, error) => {
setSearchHost(value);
}
const handleSearchValueChange = (name, value, error) => {
setSearch(value);
}
const submitSearch = (event, querySearch, querySearchHost, querySearchField) => {
let adjustedSearchField = querySearchField ? querySearchField : searchField;
let adjustedSearch = querySearch ? querySearch : search;
let adjustedSearchHost = querySearchHost ? querySearchHost : searchHost;
props.changeSearchParam("host", adjustedSearchHost);
props.changeSearchParam("search", adjustedSearch);
switch(adjustedSearchField){
case "Name":
props.onNameSearch({search:adjustedSearch, searchHost:adjustedSearchHost, offset: 0})
break;
case "Comment":
props.onCommentSearch({search:adjustedSearch, searchHost:adjustedSearchHost, offset: 0})
break;
case "Tag":
props.onTagSearch({search:adjustedSearch, searchHost:adjustedSearchHost, offset: 0});
break;
case "Metadata":
props.onOtherSearch({search:adjustedSearch, searchHost:adjustedSearchHost, offset: 0});
break;
default:
break;
}
}
useQuery(customBrowsers, {fetchPolicy: "no-cache",
onCompleted: (data) => {
setBrowserOptions(data.custombrowser);
if(data.custombrowser.length > 0){
setSelectedBrowser(data.custombrowser[0].name);
props.onChangeBrowserField(data.custombrowser[0]);
}
}
})
React.useEffect(() => {
if(props.value === props.index){
let queryParams = new URLSearchParams(window.location.search);
let adjustedSearch = "";
let adjustedSearchHost = "";
let adjustedSearchField = "Name";
if(queryParams.has("search")){
setSearch(queryParams.get("search"));
adjustedSearch = queryParams.get("search");
}
if(queryParams.has("searchField") && searchFieldOptions.includes(queryParams.get("searchField"))){
setSearchField(queryParams.get("searchField"));
props.onChangeSearchField(queryParams.get("searchField"));
adjustedSearchField = queryParams.get("searchField");
}else{
setSearchField("Name");
props.onChangeSearchField("Name");
props.changeSearchParam("searchField", "Name");
}
if(queryParams.has("host")){
setSearchHost(queryParams.get("host"));
adjustedSearchHost = queryParams.get("host")
}
submitSearch(null, adjustedSearch, adjustedSearchHost, adjustedSearchField);
}
}, [props.value, props.index]);
return (
<Grid container spacing={1} style={{padding: "5px 5px 0 5px", maxWidth: "100%"}}>
<Grid size={2}>
<Select
style={{marginBottom: "10px", width: "100%"}}
value={selectedBrowser}
onChange={handleBrowserChange}
>
{
browserOptions.map((opt, i) => (
<MenuItem key={"searchopt" + opt.name} value={opt.name}>{opt.name}</MenuItem>
))
}
</Select>
</Grid>
<Grid size={2}>
<MythicTextField placeholder="Host Name Search..." value={searchHost} marginTop={"0px"}
onChange={handleSearchHostValueChange} onEnter={submitSearch} name="Host Name Search..." />
</Grid>
<Grid size={3}>
<MythicTextField placeholder="Search..." value={search} marginTop={"0px"}
onChange={handleSearchValueChange} onEnter={submitSearch} name="Search..." InputProps={{
endAdornment:
<React.Fragment>
<Tooltip title="Search">
<IconButton onClick={submitSearch} size="large"><SearchIcon style={{color: theme.palette.info.main}}/></IconButton>
</Tooltip>
</React.Fragment>,
style: {padding: 0}
}}/>
</Grid>
<Grid size={2}>
<Select
style={{marginBottom: "10px", width: "100%"}}
value={searchField}
onChange={handleSearchFieldChange}
>
{
searchFieldOptions.map((opt, i) => (
<MenuItem key={"searchopt" + opt} value={opt}>{opt}</MenuItem>
))
}
</Select>
</Grid>
</Grid>
);
}
export const SearchTabCustomBrowserPanel = (props) =>{
const [processData, setProcessData] = React.useState([]);
const [totalCount, setTotalCount] = React.useState(0);
const [selectedBrowser, setSelectedBrowser] = React.useState({name: "", columns: []});
const [search, setSearch] = React.useState("");
const [searchField, setSearchField] = React.useState("Name");
const [searchHost, setSearchHost] = React.useState("");
const onChangeBrowserField = (field) => {
setSelectedBrowser(field);
setProcessData([]);
}
const onChangeSearchField = (field) => {
setSearchField(field);
setProcessData([]);
switch(field){
case "Name":
onNameSearch({search, searchHost, offset: 0});
break;
case "Comment":
onCommentSearch({search, searchHost, offset: 0});
break;
case "Tag":
onTagSearch({search, searchHost, offset: 0});
break;
default:
break;
}
}
const handleSearchResults = (data) => {
snackActions.dismiss();
if(searchField === "Tag"){
setTotalCount(data.tag_aggregate.aggregate.count);
setProcessData(data?.tag?.map(t => t.mythictree) || []);
} else {
setTotalCount(data.mythictree_aggregate.aggregate.count);
setProcessData(data.mythictree);
}
}
const handleCallbackSearchFailure = (data) => {
snackActions.dismiss();
snackActions.error("Failed to fetch data for search");
console.log(data);
}
const getNameSearch = useMythicLazyQuery(nameSearch, {
fetchPolicy: "no-cache"
})
const getCommentSearch = useMythicLazyQuery(commentSearch, {
fetchPolicy: "no-cache",
})
const getTagSearch = useMythicLazyQuery(tagSearch, {
fetchPolicy: "no-cache"
})
const getOtherSearch = useMythicLazyQuery(metadataSearch, {fetchPolicy: "no-cache"})
const onNameSearch = ({search, searchHost, offset}) => {
//snackActions.info("Searching...", {persist:true});
setSearch(search);
if(selectedBrowser.name === ""){return}
let new_search = search;
if(new_search === ""){
new_search = "_";
}
setSearchHost(searchHost);
getNameSearch({variables:{
offset: offset,
fetchLimit: fetchLimit,
name: "%" + new_search + "%",
host: "%" + searchHost + "%",
tree_type: selectedBrowser.name
}}).then(({data}) => handleSearchResults(data)).catch(({data}) => handleCallbackSearchFailure(data))
}
const onCommentSearch = ({search, searchHost, offset}) => {
//snackActions.info("Searching...", {persist:true});
setSearch(search);
if(selectedBrowser.name === ""){return}
let new_search = search;
if(new_search === ""){
new_search = "_";
}
setSearchHost(searchHost);
getCommentSearch({variables:{
offset: offset,
fetchLimit: fetchLimit,
comment: "%" + new_search + "%",
host: "%" + searchHost + "%",
tree_type: selectedBrowser.name
}}).then(({data}) => handleSearchResults(data)).catch(({data}) => handleCallbackSearchFailure(data))
}
const onTagSearch = ({search, searchHost, offset}) => {
//snackActions.info("Searching...", {persist:true});
setSearch(search);
if(selectedBrowser.name === ""){return}
let new_search = search;
if(new_search === ""){
new_search = "_";
}
setSearchHost(searchHost);
getTagSearch({variables:{
offset: offset,
fetchLimit: fetchLimit,
tag: "%" + new_search + "%",
host: "%" + searchHost + "%",
tree_type: selectedBrowser.name
}}).then(({data}) => handleSearchResults(data)).catch(({data}) => handleCallbackSearchFailure(data))
}
const onOtherSearch = ({search, searchHost, offset}) => {
//snackActions.info("Searching...", {persist:true});
setSearch(search);
if(selectedBrowser.name === ""){return}
let new_search = search;
if(new_search === ""){
new_search = "_";
}
setSearchHost(searchHost);
getOtherSearch({variables:{
offset: offset,
fetchLimit: fetchLimit,
search: "%" + new_search + "%",
host: "%" + searchHost + "%",
tree_type: selectedBrowser.name
}}).then(({data}) => handleSearchResults(data)).catch(({data}) => handleCallbackSearchFailure(data))
}
const onChangePage = (event, value) => {
switch(searchField){
case "Name":
onNameSearch({search, searchHost:searchHost, offset: (value - 1) * fetchLimit});
break;
case "Comment":
onCommentSearch({search, searchHost:searchHost, offset: (value - 1) * fetchLimit});
break;
case "Tag":
onTagSearch({search, searchHost:searchHost, offset: (value - 1) * fetchLimit});
break;
case "Metadata":
onOtherSearch({search, searchHost:searchHost, offset: (value - 1) * fetchLimit});
break;
default:
break;
}
}
React.useEffect( () => {
if(selectedBrowser.name === ""){return}
onChangePage(null, 1);
}, [selectedBrowser]);
return (
<MythicTabPanel {...props} >
<SearchTabCustomBrowserSearchPanel onChangeSearchField={onChangeSearchField}
onNameSearch={onNameSearch} value={props.value} index={props.index}
onCommentSearch={onCommentSearch}
onTagSearch={onTagSearch}
onOtherSearch={onOtherSearch}
onChangeBrowserField={onChangeBrowserField}
changeSearchParam={props.changeSearchParam}/>
<div style={{overflowY: "auto", flexGrow: 1}}>
{processData.length > 0 ? (
<CustomBrowserTable rows={processData} columns={selectedBrowser.columns} />) : (
<div style={{display: "flex", justifyContent: "center", alignItems: "center", position: "absolute", left: "50%", top: "50%"}}>No Search Results</div>
)}
</div>
<div style={{background: "transparent", display: "flex", justifyContent: "center", alignItems: "center"}}>
<Pagination count={Math.ceil(totalCount / fetchLimit)} variant="outlined" color="info" boundaryCount={1}
siblingCount={1} onChange={onChangePage} showFirstButton={true} showLastButton={true} style={{padding: "20px"}}/>
<Typography style={{paddingLeft: "10px"}}>Total Results: {totalCount}</Typography>
</div>
</MythicTabPanel>
)
}