link-stack/apps/leafcutter/app/_components/VisualizationBuilder.tsx

374 lines
11 KiB
TypeScript
Raw Normal View History

2023-07-18 12:26:57 +00:00
"use client";
2023-02-13 13:46:56 +00:00
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";
2023-07-18 12:26:57 +00:00
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";
2023-02-13 13:46:56 +00:00
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;
2023-05-24 20:27:57 +00:00
const [selectedVisualizationType, setSelectedVisualizationType] = useState(
null as any
);
2023-02-13 13:46:56 +00:00
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]);
2023-05-24 20:27:57 +00:00
const showSavedSearchPopup = (event: any) => {
2023-02-13 13:46:56 +00:00
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();
2023-05-24 20:27:57 +00:00
const found: any = savedSearches.find(
(search: any) => search.name === name
);
replaceQuery(found?.query);
2023-02-13 13:46:56 +00:00
};
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 />
2023-05-24 20:27:57 +00:00
{savedSearches.map((savedSearch: any) => (
2023-02-13 13:46:56 +00:00
<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}
2023-05-24 20:27:57 +00:00
// @ts-expect-error
2023-02-13 13:46:56 +00:00
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", "")
2023-05-24 20:27:57 +00:00
// @ts-expect-error
2023-02-13 13:46:56 +00:00
.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>
);
};