373 lines
11 KiB
TypeScript
373 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { FC, useState, useEffect } from "react";
|
|
import {
|
|
Box,
|
|
Button,
|
|
Grid,
|
|
Popover,
|
|
Accordion,
|
|
AccordionSummary,
|
|
AccordionDetails,
|
|
Dialog,
|
|
Divider,
|
|
Paper,
|
|
MenuList,
|
|
MenuItem,
|
|
ListItemText,
|
|
ListItemIcon,
|
|
TextField,
|
|
} from "@mui/material";
|
|
import {
|
|
ExpandMore as ExpandMoreIcon,
|
|
AddCircleOutline as AddCircleOutlineIcon,
|
|
SavedSearch as SavedSearchIcon,
|
|
RemoveCircle as RemoveCircleIcon,
|
|
} from "@mui/icons-material";
|
|
import { useTranslate } from "react-polyglot";
|
|
import { QueryBuilder } from "app/_components/QueryBuilder";
|
|
import { QueryText } from "app/_components/QueryText";
|
|
import { LiveDataViewer } from "app/_components/LiveDataViewer";
|
|
import { Tooltip } from "app/_components/Tooltip";
|
|
import visualizationMap from "app/_config/visualizationMap.json";
|
|
import { VisualizationSelectCard } from "./VisualizationSelectCard";
|
|
import { MetricSelectCard } from "./MetricSelectCard";
|
|
import { useAppContext } from "./AppProvider";
|
|
|
|
interface VisualizationBuilderProps {
|
|
templates: any[];
|
|
}
|
|
|
|
export const VisualizationBuilder: FC<VisualizationBuilderProps> = ({
|
|
templates,
|
|
}) => {
|
|
const t = useTranslate();
|
|
const {
|
|
typography: { h4 },
|
|
colors: { white, leafcutterElectricBlue, cdrLinkOrange },
|
|
foundCount,
|
|
query,
|
|
replaceQuery,
|
|
clearQuery,
|
|
} = useAppContext();
|
|
const { visualizations } = visualizationMap;
|
|
const [selectedVisualizationType, setSelectedVisualizationType] = useState(
|
|
null as any
|
|
);
|
|
const toggleSelectedVisualizationType = (visualizationType: string) => {
|
|
if (visualizationType === selectedVisualizationType) {
|
|
setSelectedVisualizationType(null);
|
|
} else {
|
|
setSelectedVisualizationType(visualizationType);
|
|
}
|
|
};
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [savedSearches, setSavedSearches] = useState([]);
|
|
const [savedSearchName, setSavedSearchName] = useState("");
|
|
const [anchorEl, setAnchorEl] = useState(null);
|
|
|
|
const updateSearches = async () => {
|
|
const result = await fetch("/api/searches/list");
|
|
const existingSearches = await result.json();
|
|
setSavedSearches(existingSearches);
|
|
};
|
|
useEffect(() => {
|
|
updateSearches();
|
|
}, [setSavedSearches]);
|
|
|
|
const showSavedSearchPopup = (event: any) => {
|
|
setAnchorEl(event.currentTarget);
|
|
};
|
|
const handleClose = () => {
|
|
setSavedSearchName("");
|
|
setAnchorEl(null);
|
|
};
|
|
const closeDialog = () => {
|
|
setDialogOpen(false);
|
|
};
|
|
const createSavedSearch = async (name: string, q: any) => {
|
|
await fetch("/api/searches/create", {
|
|
method: "POST",
|
|
body: JSON.stringify({ name, query: q }),
|
|
});
|
|
await updateSearches();
|
|
handleClose();
|
|
closeDialog();
|
|
};
|
|
|
|
const deleteSavedSearch = async (name: string) => {
|
|
await fetch("/api/searches/delete", {
|
|
method: "POST",
|
|
body: JSON.stringify({ name }),
|
|
});
|
|
await updateSearches();
|
|
closeDialog();
|
|
};
|
|
|
|
const updateSearch = (name: string) => {
|
|
handleClose();
|
|
closeDialog();
|
|
const found: any = savedSearches.find(
|
|
(search: any) => search.name === name
|
|
);
|
|
replaceQuery(found?.query);
|
|
};
|
|
|
|
const clearSearch = () => clearQuery();
|
|
|
|
const open = Boolean(anchorEl);
|
|
const elementID = open ? "simple-popover" : undefined;
|
|
const [queryExpanded, setQueryExpanded] = useState(true);
|
|
const [resultsExpanded, setResultsExpanded] = useState(false);
|
|
const minHeight = "42px";
|
|
const maxHeight = "42px";
|
|
const summaryStyles = {
|
|
backgroundColor: leafcutterElectricBlue,
|
|
height: "14px",
|
|
minHeight,
|
|
maxHeight,
|
|
"&.Mui-expanded": {
|
|
minHeight,
|
|
maxHeight,
|
|
},
|
|
};
|
|
const buttonStyles = {
|
|
fontFamily: "Poppins, sans-serif",
|
|
fontWeight: 700,
|
|
color: `${white} !important`,
|
|
borderRadius: 999,
|
|
backgroundColor: leafcutterElectricBlue,
|
|
padding: "6px 30px",
|
|
margin: "20px 0px",
|
|
whiteSpace: "nowrap",
|
|
};
|
|
|
|
return (
|
|
<Box>
|
|
<Dialog open={dialogOpen}>
|
|
<Box sx={{ pt: 3, pl: 3, pr: 3 }}>
|
|
<Grid container direction="column" spacing={2}>
|
|
<Grid item>
|
|
<TextField
|
|
size="small"
|
|
placeholder="Saved search name"
|
|
sx={{ width: 400 }}
|
|
onChange={(e) => setSavedSearchName(e.target.value)}
|
|
/>
|
|
</Grid>
|
|
<Grid item container direction="row" justifyContent="space-between">
|
|
<Grid item>
|
|
<Button sx={buttonStyles} onClick={closeDialog}>
|
|
Cancel
|
|
</Button>
|
|
</Grid>
|
|
<Grid item>
|
|
<Button
|
|
sx={buttonStyles}
|
|
onClick={async () => {
|
|
await createSavedSearch(savedSearchName, query);
|
|
}}
|
|
>
|
|
Save
|
|
</Button>
|
|
</Grid>
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
</Dialog>
|
|
<Grid
|
|
container
|
|
direction="row"
|
|
sx={{ mt: 4, mb: 2 }}
|
|
justifyContent="space-between"
|
|
>
|
|
<Grid item>
|
|
<Tooltip
|
|
title={t("searchAndCreateTitle")}
|
|
description={t("searchAndCreateDescription")}
|
|
tooltipID="searchCreate"
|
|
nextURL="/create?tooltip=categories"
|
|
previousURL="/?tooltip=profile"
|
|
placement="top"
|
|
>
|
|
<Box sx={h4}>Search Criteria</Box>
|
|
</Tooltip>
|
|
</Grid>
|
|
<Grid item>
|
|
<Button
|
|
aria-describedby={elementID}
|
|
variant="contained"
|
|
onClick={showSavedSearchPopup}
|
|
sx={{
|
|
backgroundColor: cdrLinkOrange,
|
|
textTransform: "none",
|
|
fontStyle: "italic",
|
|
fontWeight: "bold",
|
|
}}
|
|
>
|
|
<SavedSearchIcon sx={{ mr: 1 }} />
|
|
{t("savedSearch")}
|
|
</Button>
|
|
<Button
|
|
variant="contained"
|
|
onClick={clearSearch}
|
|
sx={{
|
|
backgroundColor: leafcutterElectricBlue,
|
|
textTransform: "none",
|
|
fontWeight: "bold",
|
|
ml: 3,
|
|
}}
|
|
>
|
|
{t("clear")}
|
|
</Button>
|
|
<Popover
|
|
id={elementID}
|
|
open={open}
|
|
anchorEl={anchorEl}
|
|
onClose={handleClose}
|
|
anchorOrigin={{
|
|
vertical: "bottom",
|
|
horizontal: "right",
|
|
}}
|
|
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
|
>
|
|
<Paper>
|
|
<MenuList>
|
|
<MenuItem
|
|
onClick={() => {
|
|
handleClose();
|
|
setDialogOpen(true);
|
|
}}
|
|
>
|
|
<ListItemIcon>
|
|
<AddCircleOutlineIcon fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("saveCurrentSearch")}</ListItemText>
|
|
</MenuItem>
|
|
<Divider />
|
|
{savedSearches.map((savedSearch: any) => (
|
|
<MenuItem
|
|
key={savedSearch.name}
|
|
onClick={() => updateSearch(savedSearch.name)}
|
|
>
|
|
<ListItemIcon />
|
|
<ListItemText>{savedSearch.name}</ListItemText>
|
|
<Box
|
|
onClick={() => deleteSavedSearch(savedSearch.name)}
|
|
sx={{ p: 0, m: 0, zIndex: 100 }}
|
|
>
|
|
<RemoveCircleIcon
|
|
sx={{
|
|
color: cdrLinkOrange,
|
|
p: 0,
|
|
m: 0,
|
|
":hover": { color: leafcutterElectricBlue },
|
|
}}
|
|
/>
|
|
</Box>
|
|
</MenuItem>
|
|
))}
|
|
</MenuList>
|
|
</Paper>
|
|
</Popover>
|
|
</Grid>
|
|
</Grid>
|
|
<QueryBuilder />
|
|
<Accordion
|
|
expanded={queryExpanded}
|
|
onClick={() => setQueryExpanded(!queryExpanded)}
|
|
>
|
|
<AccordionSummary
|
|
expandIcon={<ExpandMoreIcon sx={{ color: white, fontSize: 28 }} />}
|
|
sx={summaryStyles}
|
|
>
|
|
<Box sx={{ ...h4, color: white }}>{t("query")}</Box>
|
|
</AccordionSummary>
|
|
<AccordionDetails sx={{ p: 2 }}>
|
|
<QueryText />
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
<Accordion
|
|
sx={{ mt: 2 }}
|
|
expanded={resultsExpanded}
|
|
onClick={() => setResultsExpanded(!resultsExpanded)}
|
|
>
|
|
<AccordionSummary
|
|
expandIcon={<ExpandMoreIcon sx={{ color: white, fontSize: 28 }} />}
|
|
sx={summaryStyles}
|
|
>
|
|
<Box sx={{ ...h4, color: white }}>{`${t(
|
|
"results"
|
|
)} (${foundCount})`}</Box>
|
|
</AccordionSummary>
|
|
<Tooltip
|
|
title={`${t("queryResultsCardTitle")}`}
|
|
description={t("queryResultsCardDescription")}
|
|
tooltipID="queryResults"
|
|
placement="top"
|
|
previousURL="/create?tooltip=advancedOptions"
|
|
nextURL="/create?tooltip=viewResults"
|
|
>
|
|
<AccordionDetails sx={{ p: 2, pb: 4 }}>
|
|
<LiveDataViewer />
|
|
</AccordionDetails>
|
|
</Tooltip>
|
|
</Accordion>
|
|
<Tooltip
|
|
title={t("viewResultsCardTitle")}
|
|
description={t("viewResultsCardDescription")}
|
|
tooltipID="viewResults"
|
|
placement="top"
|
|
previousURL="/create?tooltip=queryResults"
|
|
>
|
|
<Box sx={{ ...h4, mt: 6, mb: 2 }}>{t("selectVisualization")}:</Box>
|
|
</Tooltip>
|
|
<Box display="grid" gridTemplateColumns="repeat(5, 1fr)" gap={2}>
|
|
{Object.keys(visualizations).map((key: string) => (
|
|
<VisualizationSelectCard
|
|
key={key}
|
|
visualizationType={key}
|
|
// @ts-expect-error
|
|
title={visualizations[key].name}
|
|
enabled={
|
|
selectedVisualizationType === key ||
|
|
selectedVisualizationType === null
|
|
}
|
|
selected={selectedVisualizationType === key}
|
|
toggleSelected={toggleSelectedVisualizationType}
|
|
/>
|
|
))}
|
|
</Box>
|
|
<Box sx={{ ...h4, mt: 6, mb: 2 }}>{t("selectFieldVisualize")}:</Box>
|
|
<Box
|
|
display="grid"
|
|
gridTemplateColumns="repeat(5, 1fr)"
|
|
gap={2}
|
|
sx={{ minHeight: 200 }}
|
|
>
|
|
{templates
|
|
.filter(
|
|
(template: any) => template.type === selectedVisualizationType
|
|
)
|
|
.map((template: any) => {
|
|
const { id, type, title, description } = template;
|
|
const cleanTitle = title
|
|
.replace("Templated", "")
|
|
// @ts-expect-error
|
|
.replace(visualizations[type].name, "");
|
|
const metricType = cleanTitle.replace(/\s/g, "").toLowerCase();
|
|
return (
|
|
<MetricSelectCard
|
|
key={id}
|
|
visualizationID={id}
|
|
metricType={metricType}
|
|
title={`By ${cleanTitle}`}
|
|
description={description}
|
|
enabled
|
|
/>
|
|
);
|
|
})}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|