This commit is contained in:
Darren Clarke 2023-08-25 07:11:33 +00:00
parent 8f165d15d2
commit c620e4bf25
264 changed files with 9983 additions and 2280 deletions

View file

@ -1,163 +0,0 @@
"use client";
import { FC } from "react";
import { useTranslate } from "react-polyglot";
import Image from "next/legacy/image";
import Link from "next/link";
import { Grid, Container, Box, Button } from "@mui/material";
import { useAppContext } from "app/_components/AppProvider";
import { PageHeader } from "app/_components/PageHeader";
import { AboutFeature } from "./AboutFeature";
import { AboutBox } from "./AboutBox";
import AbstractDiagram from "images/abstract-diagram.png";
import AboutHeader from "images/about-header.png";
import Globe from "images/globe.png";
import Controls from "images/controls.png";
import CommunityBackground from "images/community-background.png";
import Bicycle from "images/bicycle.png";
export const About: FC = () => {
const t = useTranslate();
const {
colors: { white, leafcutterElectricBlue, cdrLinkOrange },
typography: { h1, h4, p },
} = useAppContext();
return (
<>
<PageHeader
backgroundColor={leafcutterElectricBlue}
sx={{
backgroundImage: `url(${AboutHeader.src})`,
backgroundSize: "200px",
backgroundPosition: "bottom right",
backgroundRepeat: "no-repeat",
}}
>
<Grid
container
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Grid item xs={9}>
<Box component="h1" sx={h1}>
{t("aboutLeafcutterTitle")}
</Box>
<Box component="h4" sx={{ ...h4, mt: 1, mb: 1 }}>
{t("aboutLeafcutterDescription")}
</Box>
</Grid>
</Grid>
</PageHeader>
<Container maxWidth="lg">
<AboutFeature
title={t("whatIsLeafcutterTitle")}
description={t("whatIsLeafcutterDescription")}
direction="row"
image={AbstractDiagram}
showBackground={false}
textColumns={8}
/>
<AboutFeature
title={t("whatIsItForTitle")}
description={t("whatIsItForDescription")}
direction="row-reverse"
image={Controls}
showBackground
textColumns={8}
/>
<AboutFeature
title={t("whoCanUseItTitle")}
description={t("whoCanUseItDescription")}
direction="row"
image={Globe}
showBackground
textColumns={6}
/>
</Container>
<AboutBox backgroundColor={cdrLinkOrange}>
<Box component="h4" sx={{ ...h4, mt: 0 }}>
{t("whereDataComesFromTitle")}
</Box>
{t("whereDataComesFromDescription")
.split("\n")
.map((line: string, i: number) => (
<Box component="p" key={i} sx={p}>
{line}
</Box>
))}
</AboutBox>
<AboutBox backgroundColor={leafcutterElectricBlue}>
<Box component="h4" sx={{ ...h4, mt: 0 }}>
{t("projectSupportTitle")}
</Box>
{t("projectSupportDescription")
.split("\n")
.map((line: string, i: number) => (
<Box component="p" key={i} sx={p}>
{line}
</Box>
))}
</AboutBox>
<Box
sx={{
backgroundImage: `url(${CommunityBackground.src})`,
backgroundSize: "90%",
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
position: "relative",
height: "700px",
}}
>
<Box sx={{ position: "absolute", left: 0, bottom: -20, width: 300 }}>
<Image src={Bicycle} alt="" />
</Box>
<Container
maxWidth="md"
sx={{ textAlign: "center", paddingTop: "280px" }}
>
<Box
component="h4"
sx={{ ...h4, maxWidth: 500, margin: "0 auto", mt: 3 }}
>
{t("interestedInLeafcutterTitle")}
</Box>
{t("interestedInLeafcutterDescription")
.split("\n")
.map((line: string, i: number) => (
<Box
component="p"
key={i}
sx={{ ...p, maxWidth: 500, margin: "0 auto" }}
>
{line}
</Box>
))}
<Link href="mailto:info@digiresilience.org" passHref>
<Button
sx={{
fontSize: 14,
borderRadius: 500,
color: white,
backgroundColor: cdrLinkOrange,
fontWeight: "bold",
textTransform: "uppercase",
pl: 6,
pr: 5,
mt: 4,
":hover": {
backgroundColor: leafcutterElectricBlue,
color: white,
opacity: 0.8,
},
}}
>
{t("contactUs")}
</Button>
</Link>
</Container>
</Box>
</>
);
};

View file

@ -1,35 +0,0 @@
"use client";
import { FC, PropsWithChildren } from "react";
import { Box } from "@mui/material";
import { useAppContext } from "../../../_components/AppProvider";
type AboutBoxProps = PropsWithChildren<{
backgroundColor: string;
}>;
export const AboutBox: FC<AboutBoxProps> = ({
backgroundColor,
children,
}: any) => {
const {
colors: { white },
} = useAppContext();
return (
<Box
sx={{
width: "100%",
backgroundColor,
color: white,
p: 4,
borderRadius: "10px",
mt: "66px",
mb: "22px",
textAlign: "center",
}}
>
{children}
</Box>
);
};

View file

@ -1,68 +0,0 @@
"use client";
import { FC } from "react";
import Image from "next/legacy/image";
import { Grid, Box, GridSize } from "@mui/material";
import AboutDots from "images/about-dots.png";
import { useAppContext } from "app/_components/AppProvider";
interface AboutFeatureProps {
title: string;
description: string;
direction: "row" | "row-reverse";
image: any;
showBackground: boolean;
textColumns: number;
}
export const AboutFeature: FC<AboutFeatureProps> = ({
title,
description,
direction,
image,
showBackground,
textColumns,
}) => {
const {
typography: { h2, p },
} = useAppContext();
return (
<Box
sx={{
p: "20px",
mt: "40px",
backgroundImage: showBackground ? `url(${AboutDots.src})` : "",
backgroundSize: "200px 200px",
backgroundPosition: direction === "row" ? "20% 50%" : "80% 50%",
backgroundRepeat: "no-repeat",
}}
>
<Grid
direction={direction}
container
spacing={5}
alignContent="flex-start"
>
<Grid item xs={textColumns as GridSize}>
<Box component="h2" sx={h2}>
{title}
</Box>
<Box component="p" sx={p}>
{description}
</Box>
</Grid>
<Grid
item
xs={(12 - textColumns) as GridSize}
container
direction={direction}
>
<Box sx={{ width: "150px", mt: "-20px" }}>
<Image src={image} alt="" objectFit="contain" />
</Box>
</Grid>
</Grid>
</Box>
);
};

View file

@ -1,4 +1,4 @@
import { About } from './_components/About';
import { About } from "leafcutter-common";
export default function Page() {
return <About />;

View file

@ -1,64 +0,0 @@
"use client";
import { FC, useEffect } from "react";
import { useTranslate } from "react-polyglot";
import { useRouter, usePathname } from "next/navigation";
import { Box, Grid } from "@mui/material";
import { useCookies } from "react-cookie";
import { useAppContext } from "app/_components/AppProvider";
import { PageHeader } from "app/_components/PageHeader";
import { VisualizationBuilder } from "app/_components/VisualizationBuilder";
type CreateProps = {
templates: any;
};
export const Create: FC<CreateProps> = ({ templates }) => {
const t = useTranslate();
const {
colors: { cdrLinkOrange },
typography: { h1, h4 },
} = useAppContext();
const router = useRouter();
const pathname = usePathname() ?? "";
const cookieName = "searchIntroComplete";
const [cookies, setCookie] = useCookies([cookieName]);
const searchIntroComplete = parseInt(cookies[cookieName], 10) || 0;
useEffect(() => {
if (searchIntroComplete === 0) {
setCookie(cookieName, `${1}`, { path: "/" });
router.push(`${pathname}?group=search&tooltip=1&checklist=1`);
}
}, [searchIntroComplete, router, setCookie]);
return (
<>
<PageHeader backgroundColor={cdrLinkOrange}>
<Grid container direction="row" spacing={2} alignItems="center">
{/* <Grid item xs={2} sx={{ textAlign: "center" }}>
<Image src={SearchCreateHeader} width={100} height={100} alt="" />
</Grid> */}
<Grid container direction="column" item xs={10}>
<Grid item>
<Box component="h1" sx={{ ...h1 }}>
{t("searchAndCreateTitle")}
</Box>
</Grid>
<Grid item>
<Box component="h4" sx={{ ...h4, mt: 1, mb: 1 }}>
{t("searchAndCreateSubtitle")}
</Box>
</Grid>
{/* <Grid>
<Box component="p" sx={{ ...p }}>
{t("searchAndCreateDescription")}
</Box>
</Grid> */}
</Grid>
</Grid>
</PageHeader>
<VisualizationBuilder templates={templates} />
</>
);
};

View file

@ -1,8 +1,10 @@
import { getTemplates } from "app/_lib/opensearch";
import { Create } from "./_components/Create";
import { Create } from "leafcutter-common";
export default async function Page() {
const templates = await getTemplates(100);
return <Create templates={templates} />;
}
export const dynamic = "force-dynamic";

View file

@ -1,102 +0,0 @@
"use client";
import { FC } from "react";
import { useTranslate } from "react-polyglot";
import { Box, Grid } from "@mui/material";
import { PageHeader } from "app/_components/PageHeader";
import { Question } from "app/_components/Question";
import { useAppContext } from "app/_components/AppProvider";
import FaqHeader from "images/faq-header.svg";
export const FAQ: FC = () => {
const t = useTranslate();
const {
colors: { lavender },
typography: { h1, h4, p },
} = useAppContext();
const questions = [
{
question: t("whatIsLeafcutterQuestion"),
answer: t("whatIsLeafcutterAnswer"),
},
{
question: t("whoBuiltLeafcutterQuestion"),
answer: t("whoBuiltLeafcutterAnswer"),
},
{
question: t("whoCanUseLeafcutterQuestion"),
answer: t("whoCanUseLeafcutterAnswer"),
},
{
question: t("whatCanYouDoWithLeafcutterQuestion"),
answer: t("whatCanYouDoWithLeafcutterAnswer"),
},
{
question: t("whereIsTheDataComingFromQuestion"),
answer: t("whereIsTheDataComingFromAnswer"),
},
{
question: t("whereIsTheDataStoredQuestion"),
answer: t("whereIsTheDataStoredAnswer"),
},
{
question: t("howDoWeKeepTheDataSafeQuestion"),
answer: t("howDoWeKeepTheDataSafeAnswer"),
},
{
question: t("howLongDoYouKeepTheDataQuestion"),
answer: t("howLongDoYouKeepTheDataAnswer"),
},
{
question: t("whatOrganizationsAreParticipatingQuestion"),
answer: t("whatOrganizationsAreParticipatingAnswer"),
},
{
question: t("howDidYouGetMyProfileInformationQuestion"),
answer: t("howDidYouGetMyProfileInformationAnswer"),
},
{
question: t("howCanILearnMoreAboutLeafcutterQuestion"),
answer: t("howCanILearnMoreAboutLeafcutterAnswer"),
},
];
return (
<>
<PageHeader
backgroundColor={lavender}
sx={{
backgroundImage: `url(${FaqHeader.src})`,
backgroundSize: "150px",
backgroundPosition: "bottom right",
backgroundRepeat: "no-repeat",
}}
>
<Grid
container
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Grid item>
<Box component="h1" sx={{ ...h1 }}>
{t("frequentlyAskedQuestionsTitle")}
</Box>
<Box component="h4" sx={{ ...h4, mt: 1, mb: 1 }}>
{t("frequentlyAskedQuestionsSubtitle")}
</Box>
<Box component="p" sx={{ ...p }}>
{t("frequentlyAskedQuestionsDescription")}
</Box>
</Grid>
</Grid>
</PageHeader>
{questions.map((q: any, index: number) => (
<Question key={index} question={q.question} answer={q.answer} />
))}
</>
);
};

View file

@ -1,4 +1,4 @@
import { FAQ } from "./_components/FAQ";
import { FAQ } from "leafcutter-common";
export default function Page() {
return <FAQ />;

View file

@ -7,16 +7,12 @@ import "@fontsource/roboto/700.css";
import "@fontsource/playfair-display/900.css";
// import getConfig from "next/config";
// import { LicenseInfo } from "@mui/x-data-grid-pro";
import { InternalLayout } from "app/_components/InternalLayout";
import { headers } from 'next/headers'
import { InternalLayout } from "../_components/InternalLayout";
type LayoutProps = {
children: ReactNode;
};
export default function Layout({ children }: LayoutProps) {
const allHeaders = headers();
const embedded = Boolean(allHeaders.get('x-leafcutter-embedded'));
return <InternalLayout embedded={embedded}>{children}</InternalLayout>;
return <InternalLayout embedded={false}>{children}</InternalLayout>;
}

View file

@ -1,7 +1,7 @@
import { getServerSession } from "next-auth";
import { authOptions } from "app/_lib/auth";
import { getUserVisualizations } from "app/_lib/opensearch";
import { Home } from "app/_components/Home";
import { Home } from "leafcutter-common";
export default async function Page() {
const session = await getServerSession(authOptions);

View file

@ -1,23 +0,0 @@
"use client";
import { FC } from "react";
/* eslint-disable no-underscore-dangle */
import { RawDataViewer } from "app/_components/RawDataViewer";
import { VisualizationDetail } from "app/_components/VisualizationDetail";
interface PreviewProps {
visualization: any;
visualizationType: string;
data: any[];
}
export const Preview: FC<PreviewProps> = ({
visualization,
visualizationType,
data,
}) =>
visualizationType === "rawData" ? (
<RawDataViewer rows={data} height={750} />
) : (
<VisualizationDetail {...visualization} />
);

View file

@ -1,10 +1,10 @@
/* eslint-disable no-underscore-dangle */
// import { Client } from "@opensearch-project/opensearch";
import { Preview } from "./_components/Preview";
import { Preview } from "leafcutter-common";
// import { createVisualization } from "lib/opensearch";
export default function Page() {
return <Preview visualization={undefined} visualizationType={""} data={[]}/>;
return <Preview visualization={undefined} visualizationType={""} data={[]} />;
}
/*

View file

@ -5,7 +5,7 @@ import { useLayoutEffect } from "react";
import { useRouter } from "next/navigation";
import { Grid, CircularProgress } from "@mui/material";
import Iframe from "react-iframe";
import { useAppContext } from "app/_components/AppProvider";
import { useAppContext } from "../../../_components/AppProvider";
export const Setup: FC = () => {
const {
@ -20,6 +20,7 @@ export const Setup: FC = () => {
<Grid
sx={{ width: "100%", height: 700 }}
direction="row"
container
justifyContent="space-around"
alignItems="center"
alignContent="center"

View file

@ -1,6 +1,5 @@
import { Setup } from './_components/Setup';
import { Setup } from "./_components/Setup";
export default function Page() {
return <Setup />;
}

View file

@ -1,72 +0,0 @@
"use client";
import { FC } from "react";
import { Grid, Box } from "@mui/material";
import { useTranslate } from "react-polyglot";
import { PageHeader } from "app/_components/PageHeader";
import { VisualizationCard } from "app/_components/VisualizationCard";
import { useAppContext } from "app/_components/AppProvider";
type TrendsProps = {
visualizations: any;
};
export const Trends: FC<TrendsProps> = ({ visualizations }) => {
const t = useTranslate();
const {
colors: { cdrLinkOrange },
typography: { h1, h4, p },
} = useAppContext();
return (
<>
<PageHeader backgroundColor={cdrLinkOrange}>
<Grid
container
direction="row"
spacing={2}
justifyContent="space-between"
alignItems="center"
>
{/* <Grid item xs={3} sx={{ textAlign: "center" }}>
<Image src={SearchCreateHeader} width={200} height={200} alt="" />
</Grid> */}
<Grid item container direction="column" xs={12}>
<Grid item>
<Box component="h1" sx={{ ...h1 }}>
{t("trendsTitle")}
</Box>
</Grid>
<Grid item>
<Box component="h4" sx={{ ...h4, mt: 1, mb: 1 }}>
{t("trendsSubtitle")}
</Box>
</Grid>
<Grid>
<Box component="p" sx={{ ...p }}>
{t("trendsDescription")}
</Box>
</Grid>
</Grid>
</Grid>
</PageHeader>
<Grid
container
direction="row"
wrap="wrap"
spacing={3}
justifyContent="space-between"
>
{visualizations.map((visualization: any, index: number) => (
<VisualizationCard
key={index}
id={visualization.id}
title={visualization.title}
description={visualization.description}
url={visualization.url}
/>
))}
</Grid>
</>
);
};

View file

@ -1,8 +1,10 @@
import { getTrends } from "app/_lib/opensearch";
import { Trends } from "./_components/Trends";
import { Trends } from "../../../../../packages/leafcutter-common/components/Trends";
export default async function Page() {
const visualizations = await getTrends(25);
return <Trends visualizations={visualizations} />;
}
export const dynamic = "force-dynamic";

View file

@ -1,6 +1,6 @@
/* eslint-disable no-underscore-dangle */
import { Client } from "@opensearch-project/opensearch";
import { VisualizationDetail } from "app/_components/VisualizationDetail";
import { VisualizationDetail } from "leafcutter-common";
const getVisualization = async (visualizationID: string) => {
const node = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
@ -18,7 +18,7 @@ const getVisualization = async (visualizationID: string) => {
const response = rawResponse.body;
const hits = response.hits.hits.filter(
(hit: any) => hit._id.split(":")[1] === visualizationID[0]
(hit: any) => hit._id.split(":")[1] === visualizationID[0],
);
const hit = hits[0];
const visualization = {

View file

@ -8,7 +8,7 @@ import {
useState,
PropsWithChildren,
} from "react";
import { colors, typography } from "app/_styles/theme";
import { colors, typography } from "leafcutter-common/styles/theme";
const basePath = process.env.GITLAB_CI
? "/link/link-stack/apps/leafcutter"

View file

@ -1,42 +0,0 @@
"use client";
import { FC } from "react";
import Link from "next/link";
import { Button as MUIButton } from "@mui/material";
import { useAppContext } from "./AppProvider";
interface ButtonProps {
text: string;
color: string;
href: string;
}
export const Button: FC<ButtonProps> = ({ text, color, href }) => {
const {
colors: { white, almostBlack },
} = useAppContext();
return (
<Link href={href} passHref>
<MUIButton
variant="contained"
disableElevation
sx={{
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
color:
color === white
? `${almostBlack} !important`
: `${white} !important`,
borderRadius: 999,
backgroundColor: color,
padding: "6px 30px",
margin: "20px 0px",
whiteSpace: "nowrap",
}}
>
{text}
</MUIButton>
</Link>
);
};

View file

@ -1,115 +0,0 @@
"use client";
import { FC } from "react";
import { Container, Grid, Box, Button } from "@mui/material";
import { useTranslate } from "react-polyglot";
import Image from "next/legacy/image";
import Link from "next/link";
import leafcutterLogo from "images/leafcutter-logo.png";
import footerLogo from "images/footer-logo.png";
import twitterLogo from "images/twitter-logo.png";
import gitlabLogo from "images/gitlab-logo.png";
import { useAppContext } from "./AppProvider";
export const Footer: FC = () => {
const t = useTranslate();
const {
colors: { white, leafcutterElectricBlue },
typography: { bodySmall },
} = useAppContext();
const smallLinkStyles: any = {
...bodySmall,
color: white,
textTransform: "none",
};
return (
<Box
sx={{
backgroundColor: leafcutterElectricBlue,
backgroundImage: `url(${footerLogo})`,
backgroundBlendMode: "overlay",
backgroundPosition: "bottom left",
backgroundRepeat: "no-repeat",
backgroundSize: "30%",
marginTop: "40px",
marginLeft: "300px",
}}
>
<Container sx={{ pt: 4, pb: 4 }}>
<Grid
container
direction="row"
wrap="nowrap"
justifyContent="space-between"
>
<Grid
item
container
direction="row"
wrap="nowrap"
sx={{ maxHeight: "auto" }}
>
<Grid item sx={{ width: 50, ml: 2, mr: 4 }}>
<Image src={leafcutterLogo} alt="CDR logo" />
</Grid>
</Grid>
<Grid item sx={{ color: "white" }}>
{t("contactUs")}
</Grid>
</Grid>
</Container>
<Box sx={{ backgroundColor: leafcutterElectricBlue }}>
<Container>
<Grid
item
container
direction="row"
justifyContent="space-between"
wrap="nowrap"
alignItems="center"
>
<Grid
item
container
direction="row"
spacing={1}
alignItems="center"
>
<Grid item>
<Box component="p" sx={{ ...bodySmall, color: white }}>
© {t("copyright")}
</Box>
</Grid>
<Grid item>
<Link href="/about/privacy" passHref>
<Button variant="text" sx={smallLinkStyles}>
{t("privacyPolicy")}
</Button>
</Link>
</Grid>
<Grid item>
<Link href="/about/code-practice" passHref>
<Button variant="text" sx={smallLinkStyles}>
{t("codeOfPractice")}
</Button>
</Link>
</Grid>
</Grid>
<Grid item sx={{ width: 40, p: 1, pl: 0 }}>
<a href="https://gitlab.com/digiresilience">
<Image src={gitlabLogo} alt="Gitlab logo" />
</a>
</Grid>
<Grid item sx={{ width: 40, p: 1, pr: 0 }}>
<a href="https://twitter.com/cdr_tech">
<Image src={twitterLogo} alt="Twitter logo" />
</a>
</Grid>
</Grid>
</Container>
</Box>
</Box>
);
};

View file

@ -1,138 +0,0 @@
"use client";
import { FC, useState } from "react";
import { Dialog, Box, Grid, Checkbox, IconButton } from "@mui/material";
import { Close as CloseIcon } from "@mui/icons-material";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { useTranslate } from "react-polyglot";
import { useAppContext } from "./AppProvider";
type CheckboxItemProps = {
title: string;
description: string;
checked: boolean;
onChange: () => void;
};
const CheckboxItem: FC<CheckboxItemProps> = ({
title,
description,
checked,
onChange,
}) => {
const {
typography: { p, small },
} = useAppContext();
return (
<Grid item container spacing={0}>
<Grid
item
container
spacing={0}
sx={{ backgroundColor: "white" }}
wrap="nowrap"
>
<Grid item xs={1}>
<Checkbox checked={checked} onChange={onChange} sx={{ mt: "-8px" }} />
</Grid>
<Grid
item
container
direction="column"
spacing={0}
xs={11}
sx={{ pl: 2 }}
>
<Grid item>
<Box sx={{ ...p, fontWeight: "bold" }}>{title}</Box>
</Grid>
<Grid item>
<Box sx={small}>{description}</Box>
</Grid>
</Grid>
</Grid>
</Grid>
);
};
export const GettingStartedDialog: FC = () => {
const {
colors: { almostBlack },
typography: { h4 },
} = useAppContext();
const t = useTranslate();
const router = useRouter();
const [completedItems, setCompletedItems] = useState([] as any[]);
const searchParams = useSearchParams();
const pathname = usePathname() ?? "";
const open = searchParams?.get("tooltip")?.toString() === "checklist";
const toggleCompletedItem = (item: any) => {
if (completedItems.includes(item)) {
setCompletedItems(completedItems.filter((i) => i !== item));
} else {
setCompletedItems([...completedItems, item]);
}
};
return (
<Dialog
open={open}
maxWidth="xs"
PaperProps={{
sx: { position: "absolute", bottom: 8, right: 8, borderRadius: 3 },
}}
hideBackdrop
disableEnforceFocus
disableAutoFocus
onBackdropClick={undefined}
>
<Grid container direction="column" spacing={2} sx={{ p: 3 }}>
<Grid item>
<Grid container direction="row" justifyContent="space-between">
<Grid item>
<Box sx={{ ...h4, mb: 3 }}>{t("getStartedChecklist")}</Box>
</Grid>
<Grid item>
<IconButton onClick={() => router.push(pathname ?? "")}>
<CloseIcon sx={{ color: almostBlack, fontSize: "18px" }} />
</IconButton>
</Grid>
</Grid>
<Grid container direction="column" spacing={2}>
<CheckboxItem
title={t("searchTitle")}
description={t("searchDescription")}
checked={completedItems.includes("search")}
onChange={() => toggleCompletedItem("search")}
/>
<CheckboxItem
title={t("createVisualizationTitle")}
description={t("createVisualizationDescription")}
checked={completedItems.includes("create")}
onChange={() => toggleCompletedItem("create")}
/>
<CheckboxItem
title={t("saveTitle")}
description={t("saveDescription")}
checked={completedItems.includes("save")}
onChange={() => toggleCompletedItem("save")}
/>
<CheckboxItem
title={t("exportTitle")}
description={t("exportDescription")}
checked={completedItems.includes("export")}
onChange={() => toggleCompletedItem("export")}
/>
<CheckboxItem
title={t("shareTitle")}
description={t("shareDescription")}
checked={completedItems.includes("share")}
onChange={() => toggleCompletedItem("share")}
/>
</Grid>
</Grid>
</Grid>
</Dialog>
);
};

View file

@ -1,100 +0,0 @@
"use client";
import { useEffect, FC } from "react";
import { useRouter, usePathname } from "next/navigation";
import Link from "next/link";
import ReactMarkdown from "react-markdown";
import { Grid, Button } from "@mui/material";
import { useTranslate } from "react-polyglot";
import { useCookies } from "react-cookie";
import { Welcome } from "app/_components/Welcome";
import { WelcomeDialog } from "app/_components/WelcomeDialog";
import { VisualizationCard } from "app/_components/VisualizationCard";
import { useAppContext } from "app/_components/AppProvider";
type HomeProps = {
visualizations: any;
};
export const Home: FC<HomeProps> = ({ visualizations }) => {
const router = useRouter();
const pathname = usePathname() ?? "";
const cookieName = "homeIntroComplete";
const [cookies, setCookie] = useCookies([cookieName]);
const t = useTranslate();
const {
colors: { white, leafcutterElectricBlue },
typography: { h4 },
} = useAppContext();
const homeIntroComplete = parseInt(cookies[cookieName], 10) || 0;
useEffect(() => {
if (homeIntroComplete === 0) {
setCookie(cookieName, `${1}`, { path: "/" });
router.push(`${pathname}?tooltip=welcome`);
}
}, [homeIntroComplete, router, setCookie]);
return (
<>
<Welcome />
<Grid
container
spacing={3}
sx={{ pt: "22px", pb: "22px" }}
direction="row-reverse"
>
<Link href="/create" passHref>
<Button
sx={{
fontSize: 14,
borderRadius: 500,
color: leafcutterElectricBlue,
border: `2px solid ${leafcutterElectricBlue}`,
fontWeight: "bold",
textTransform: "uppercase",
pl: 6,
pr: 5,
":hover": {
backgroundColor: leafcutterElectricBlue,
color: white,
opacity: 0.8,
},
}}
>
{t("createVisualization")}
</Button>
</Link>
</Grid>
<Grid
container
direction="row"
wrap="wrap"
spacing={3}
justifyContent="space-between"
>
{visualizations.length === 0 ? (
<Grid
container
sx={{ height: 300, width: "100%", pt: 10 }}
justifyContent="center"
>
<Grid item sx={{ ...h4, width: 450, textAlign: "center" }}>
<ReactMarkdown>{t("noSavedVisualizations")}</ReactMarkdown>
</Grid>
</Grid>
) : null}
{visualizations.map((visualization: any, index: number) => (
<VisualizationCard
id={visualization.id}
key={index}
title={visualization.title}
description={visualization.description}
url={visualization.url}
/>
))}
</Grid>
<WelcomeDialog />
</>
);
};

View file

@ -7,7 +7,7 @@ import CookieConsent from "react-cookie-consent";
import { useCookies } from "react-cookie";
import { TopNav } from "./TopNav";
import { Sidebar } from "./Sidebar";
import { GettingStartedDialog } from "./GettingStartedDialog";
import { GettingStartedDialog } from "leafcutter-common";
import { useAppContext } from "./AppProvider";
// import { Footer } from "./Footer";

View file

@ -1,24 +0,0 @@
"use client";
import { FC, useEffect, useState } from "react";
import { useAppContext } from "./AppProvider";
import { RawDataViewer } from "./RawDataViewer";
export const LiveDataViewer: FC = () => {
const { query, setFoundCount } = useAppContext();
const [rows, setRows] = useState<any[]>([]);
const searchQuery = encodeURI(JSON.stringify(query));
useEffect(() => {
const fetchData = async () => {
const result = await fetch(
`/api/visualizations/query?searchQuery=${searchQuery}`
);
const json = await result.json();
setRows(json);
setFoundCount(json?.length ?? 0);
};
fetchData();
}, [searchQuery, setFoundCount]);
return <RawDataViewer rows={rows} height={350} />;
};

View file

@ -1,150 +0,0 @@
"use client";
import { FC, useState } from "react";
import { Card, Grid } from "@mui/material";
import {
PrivacyTip as PrivacyTipIcon,
PhoneIphone as PhoneIphoneIcon,
Map as MapIcon,
Group as GroupIcon,
DateRange as DateRangeIcon,
Public as PublicIcon,
} from "@mui/icons-material";
import { VisualizationDetailDialog } from "./VisualizationDetailDialog";
import { useAppContext } from "./AppProvider";
interface MetricSelectCardProps {
visualizationID: string;
metricType: string;
title: string;
description: string;
enabled: boolean;
}
export const MetricSelectCard: FC<MetricSelectCardProps> = ({
visualizationID,
metricType,
title,
description,
enabled,
}) => {
const [open, setOpen] = useState(false);
const closeDialog = () => setOpen(false);
const [dialogParams, setDialogParams] = useState<any>({});
const {
typography: { small },
colors: { white, leafcutterElectricBlue, cdrLinkOrange },
query,
} = useAppContext();
/* const images = {
actor: PrivacyTipIcon,
incidenttype: PrivacyTipIcon,
channel: PrivacyTipIcon,
date: DateRangeIcon,
targetedgroup: GroupIcon,
impactedtechnology: PhoneIphoneIcon,
location: MapIcon,
}; */
const createAndOpen = async () => {
const createParams = {
visualizationID,
title,
description,
query,
};
const result: any = await fetch(`/api/visualizations/create`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(createParams),
});
const { id } = await result.json();
const params = {
id,
title: createParams.title,
description: createParams.description,
url: `/app/visualize?security_tenant=private#/edit/${id}?embed=true`,
};
setDialogParams(params);
setOpen(true);
};
return (
<>
<Card
sx={{
height: "100px",
backgroundColor: enabled ? leafcutterElectricBlue : white,
borderRadius: "10px",
padding: "10px",
opacity: enabled ? 1 : 0.5,
cursor: enabled ? "pointer" : "default",
"&:hover": {
backgroundColor: enabled ? cdrLinkOrange : white,
},
}}
elevation={enabled ? 2 : 0}
onClick={createAndOpen}
>
<Grid
direction="column"
container
justifyContent="space-around"
alignContent="center"
alignItems="center"
wrap="nowrap"
sx={{ height: "100%" }}
spacing={0}
>
<Grid
item
sx={{
...small,
textAlign: "center",
color: enabled ? white : leafcutterElectricBlue,
}}
>
{title}
</Grid>
<Grid item>
{metricType === "impactedtechnology" && (
<PhoneIphoneIcon fontSize="large" sx={{ color: "white" }} />
)}
{metricType === "region" && (
<PublicIcon fontSize="large" sx={{ color: "white" }} />
)}
{metricType === "continent" && (
<PublicIcon fontSize="large" sx={{ color: "white" }} />
)}
{metricType === "country" && (
<MapIcon fontSize="large" sx={{ color: "white" }} />
)}
{metricType === "targetedgroup" && (
<GroupIcon fontSize="large" sx={{ color: "white" }} />
)}
{metricType === "incidenttype" && (
<PrivacyTipIcon fontSize="large" sx={{ color: "white" }} />
)}
{metricType === "date" && (
<DateRangeIcon fontSize="large" sx={{ color: "white" }} />
)}
</Grid>
</Grid>
</Card>
{open ? (
<VisualizationDetailDialog
id={dialogParams.id}
title={dialogParams.title}
description={dialogParams.description}
url={dialogParams.url}
closeDialog={closeDialog}
editing
/>
) : null}
</>
);
};

View file

@ -8,10 +8,10 @@ import { CookiesProvider } from "react-cookie";
import { I18n } from "react-polyglot";
import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers-pro";
import { AppProvider } from "app/_components/AppProvider";
import { AppProvider } from "./AppProvider";
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
import en from "app/_locales/en.json";
import fr from "app/_locales/fr.json";
import en from "leafcutter-common/locales/en.json";
import fr from "leafcutter-common/locales/fr.json";
import "@fontsource/poppins/400.css";
import "@fontsource/poppins/700.css";
import "@fontsource/roboto/400.css";
@ -21,7 +21,7 @@ import "app/_styles/global.css";
import { LicenseInfo } from "@mui/x-date-pickers-pro";
LicenseInfo.setLicenseKey(
"7c9bf25d9e240f76e77cbf7d2ba58a23Tz02NjU4OCxFPTE3MTU4NjIzMzQ2ODgsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI="
"7c9bf25d9e240f76e77cbf7d2ba58a23Tz02NjU4OCxFPTE3MTU4NjIzMzQ2ODgsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI=",
);
const messages: any = { en, fr };

View file

@ -1,44 +0,0 @@
"use client";
import { FC } from "react";
import Iframe from "react-iframe";
import { Box } from "@mui/material";
interface OpenSearchWrapperProps {
url: string;
marginTop: string;
}
export const OpenSearchWrapper: FC<OpenSearchWrapperProps> = ({
url,
marginTop,
}) => (
<Box sx={{ position: "relative", marginTop: "-100px" }}>
<Box
sx={{
width: "100%",
height: "100px",
marginTop: "-20px",
backgroundColor: "white",
zIndex: 100,
position: "relative",
}}
/>
<Box
sx={{
marginTop,
zIndex: 1,
position: "relative",
height: "100vh",
}}
>
<Iframe
id="opensearch"
url={`${process.env.NEXT_PUBLIC_NEXTAUTH_URL}${url}&_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!t%2Cvalue%3A0)%2Ctime%3A(from%3Anow-3y%2Cto%3Anow))`}
width="100%"
height="100%"
frameBorder={0}
/>
</Box>
</Box>
);

View file

@ -1,40 +0,0 @@
"use client";
/* eslint-disable react/require-default-props */
import { FC, PropsWithChildren } from "react";
import { Box } from "@mui/material";
import { useAppContext } from "./AppProvider";
type PageHeaderProps = PropsWithChildren<{
backgroundColor: string;
sx?: any;
}>;
export const PageHeader: FC<PageHeaderProps> = ({
backgroundColor,
sx = {},
children,
}: any) => {
const {
colors: { white },
} = useAppContext();
return (
<Box
sx={{
width: "100%",
backgroundColor,
color: white,
p: 3,
borderRadius: "10px",
mb: "22px",
minHeight: "100px",
zIndex: 1000,
position: "relative",
...sx,
}}
>
{children}
</Box>
);
};

View file

@ -1,251 +0,0 @@
"use client";
import { FC, useState } from "react";
import {
Box,
Grid,
Dialog,
DialogActions,
Button,
DialogContent,
} from "@mui/material";
import {
PrivacyTip as PrivacyTipIcon,
DateRange as DateRangeIcon,
PhoneIphone as PhoneIphoneIcon,
Map as MapIcon,
Group as GroupIcon,
} from "@mui/icons-material";
import { useTranslate } from "react-polyglot";
import taxonomy from "app/_config/taxonomy.json";
import { QueryBuilderSection } from "./QueryBuilderSection";
import { QueryListSelector } from "./QueryListSelector";
import { QueryDateRangeSelector } from "./QueryDateRangeSelector";
import { useAppContext } from "./AppProvider";
import { Tooltip } from "./Tooltip";
interface QueryBuilderProps {}
export const QueryBuilder: FC<QueryBuilderProps> = () => {
const t = useTranslate();
const [dialogOpen, setDialogOpen] = useState(false);
const {
typography: { p },
colors: { leafcutterElectricBlue, mediumGray, almostBlack },
} = useAppContext();
const openAdvancedOptions = () => {
setDialogOpen(false);
window.open(`/app/visualize`, "_ blank");
};
return (
<Box sx={{ mb: 6 }}>
<Grid container direction="row" spacing={2}>
<Tooltip
title={t("categoriesCardTitle")}
description={t("categoriesCardDescription")}
tooltipID="categories"
placement="left"
previousURL="/create?tooltip=searchCreate"
nextURL="/create?tooltip=dateRange"
>
<Box sx={{ width: 0 }} />
</Tooltip>
<QueryBuilderSection
width={4}
name={t("incidentType")}
keyName="incidentType"
Image={PrivacyTipIcon}
showQueryType
tooltipTitle={t("incidentTypeCardTitle")}
tooltipDescription={t("incidentTypeCardDescription")}
>
<Grid container>
<QueryListSelector
title={t("type")}
keyName="incidentType"
values={taxonomy.incidentType}
width={12}
/>
</Grid>
</QueryBuilderSection>
<Tooltip
title={t("dateRangeCardTitle")}
description={t("dateRangeCardDescription")}
tooltipID="dateRange"
placement="top"
previousURL="/create?tooltip=categories"
nextURL="/create?tooltip=subcategories"
>
<Box sx={{ width: 0 }} />
</Tooltip>
<QueryBuilderSection
width={4}
name={t("date")}
keyName="date"
Image={DateRangeIcon}
tooltipTitle={t("dateRangeCardTitle")}
tooltipDescription={t("dateRangeCardDescription")}
>
<QueryDateRangeSelector />
</QueryBuilderSection>
<QueryBuilderSection
width={4}
name={t("targetedGroup")}
keyName="targetedGroup"
Image={GroupIcon}
showQueryType
tooltipTitle={t("targetedGroupCardTitle")}
tooltipDescription={t("targetedGroupCardDescription")}
>
<Grid container>
<QueryListSelector
title={t("group")}
keyName="targetedGroup"
values={taxonomy.targetedGroup}
width={12}
/>
</Grid>
</QueryBuilderSection>
<Tooltip
title={t("subcategoriesCardTitle")}
description={t("subcategoriesCardDescription")}
tooltipID="subcategories"
placement="top"
previousURL="/create?tooltip=dateRange"
nextURL="/create?tooltip=advancedOptions"
>
<Box sx={{ width: 0 }} />
</Tooltip>
<QueryBuilderSection
width={12}
name={t("impactedTechnology")}
keyName="impactedTechnology"
Image={PhoneIphoneIcon}
showQueryType
tooltipTitle={t("impactedTechnologyCardTitle")}
tooltipDescription={t("impactedTechnologyCardDescription")}
>
<Grid container spacing={2}>
<QueryListSelector
title={t("platform")}
keyName="platform"
values={taxonomy.platform}
width={3}
/>
<QueryListSelector
title={t("device")}
keyName="device"
values={taxonomy.device}
width={3}
/>
<QueryListSelector
title={t("service")}
keyName="service"
values={taxonomy.service}
width={3}
/>
<QueryListSelector
title={t("maker")}
keyName="maker"
values={taxonomy.maker}
width={3}
/>
</Grid>
</QueryBuilderSection>
<QueryBuilderSection
width={12}
name={t("region")}
keyName="subregion"
Image={MapIcon}
showQueryType={false}
tooltipTitle={t("regionCardTitle")}
tooltipDescription={t("regionCardDescription")}
>
<Grid container spacing={2}>
<QueryListSelector
title={t("continent")}
keyName="continent"
values={taxonomy.continent}
width={4}
/>
<QueryListSelector
title={t("country")}
keyName="country"
values={taxonomy.country}
width={4}
/>
<QueryListSelector
title={t("subregion")}
keyName="subregion"
values={taxonomy.subregion}
width={4}
/>
</Grid>
</QueryBuilderSection>
<Grid item xs={12}>
<Tooltip
title={t("advancedOptionsCardTitle")}
description={t("advancedOptionsCardDescription")}
tooltipID="advancedOptions"
placement="top"
previousURL="/create?tooltip=subcategories"
nextURL="/create?tooltip=queryResults"
>
<Button
sx={{
...p,
color: leafcutterElectricBlue,
textDecoration: "underline",
textTransform: "none",
fontWeight: "bold",
}}
onClick={() => setDialogOpen(true)}
>
{`+ ${t("advancedOptions")}`}
</Button>
</Tooltip>
</Grid>
</Grid>
<Dialog open={dialogOpen}>
<DialogContent sx={{ maxWidth: 350 }}>
{t("fullInterfaceWillOpen")}
</DialogContent>
<DialogActions>
<Grid
container
direction="row"
justifyContent="space-between"
wrap="nowrap"
sx={{ pl: 2, pr: 2, pt: 1, pb: 1 }}
>
<Grid item>
<Button
sx={{
backgroundColor: mediumGray,
color: almostBlack,
}}
variant="contained"
size="small"
onClick={() => setDialogOpen(false)}
>
{t("cancel")}
</Button>
</Grid>
<Grid item>
<Button
sx={{ backgroundColor: leafcutterElectricBlue }}
variant="contained"
size="small"
onClick={openAdvancedOptions}
>
{t("open")}
</Button>
</Grid>
</Grid>
</DialogActions>
</Dialog>
</Box>
);
};

View file

@ -1,230 +0,0 @@
"use client";
import { FC, PropsWithChildren, useState } from "react";
import {
Box,
Grid,
Accordion,
AccordionSummary,
AccordionDetails,
Button,
ButtonGroup,
IconButton,
Tooltip as MUITooltip,
} from "@mui/material";
import { useTranslate } from "react-polyglot";
import {
ExpandMore as ExpandMoreIcon,
Help as HelpIcon,
} from "@mui/icons-material";
import { useAppContext } from "./AppProvider";
interface QueryBuilderSectionProps {
name: string;
keyName: string;
children: any;
Image: any;
width: number;
// eslint-disable-next-line react/require-default-props
showQueryType?: boolean;
tooltipTitle: string;
tooltipDescription: string;
}
type TooltipProps = PropsWithChildren<{
title: string;
description: string;
children: any;
open: boolean;
}>;
const Tooltip: FC<TooltipProps> = ({ title, description, children, open }) => {
const {
colors: { white, leafcutterElectricBlue, almostBlack },
typography: { h5, small },
} = useAppContext();
return (
<MUITooltip
open={open}
title={
<Box sx={{ width: 300, p: 2, pt: 1 }}>
<Grid container direction="column">
<Grid
item
sx={{
...h5,
textTransform: "none",
textAlign: "left",
fontWeight: 700,
ml: 0,
color: leafcutterElectricBlue,
}}
>
{title}
</Grid>
<Grid item sx={{ ...small, color: almostBlack }}>
{description}
</Grid>
</Grid>
</Box>
}
arrow
placement="top"
componentsProps={{
tooltip: {
sx: {
backgroundColor: white,
boxShadow: "0px 6px 8px rgba(0,0,0,0.5)",
},
},
arrow: {
sx: {
color: "white",
fontSize: "22px",
},
},
}}
>
{children}
</MUITooltip>
);
};
export const QueryBuilderSection: FC<QueryBuilderSectionProps> = ({
name,
keyName,
children,
Image,
width,
showQueryType = false,
tooltipTitle,
tooltipDescription,
}) => {
const t = useTranslate();
const [queryType, setQueryType] = useState("include");
const [showTooltip, setShowTooltip] = useState(false);
const {
colors: { white, leafcutterElectricBlue, warningPink, almostBlack },
typography: { h6, small },
updateQueryType,
} = useAppContext();
const updateType = (type: string) => {
setQueryType(type);
updateQueryType({
[keyName]: { queryType: type },
});
};
const minHeight = "42px";
const maxHeight = "42px";
return (
<Grid item xs={width}>
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ color: white, fontSize: 28 }} />}
sx={{
backgroundColor: leafcutterElectricBlue,
height: "14px",
minHeight,
maxHeight,
"&.Mui-expanded": {
minHeight,
maxHeight,
},
}}
>
<Grid container direction="row" alignItems="center">
<Grid item>
<Image
sx={{ color: white, fontSize: 24, mr: "8px", mt: "2px" }}
alt=""
/>
</Grid>
<Grid item>
<Box sx={{ ...h6, color: white, fontWeight: "bold", mt: "-2px" }}>
{name}
</Box>
</Grid>
<Grid item>
<Tooltip
open={showTooltip}
title={tooltipTitle}
description={tooltipDescription}
>
<IconButton
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
<HelpIcon
sx={{ color: white, width: "14px", mt: "-1px", ml: "-3px" }}
/>
</IconButton>
</Tooltip>
</Grid>
</Grid>
</AccordionSummary>
<AccordionDetails>
{showQueryType ? (
<Grid
container
direction="row"
spacing={1}
sx={{ mt: 0, mb: 2 }}
justifyContent="center"
>
<Grid item sx={{ mt: "-6px" }}>
<ButtonGroup>
<Button
variant="contained"
size="small"
sx={{
fontSize: 10,
height: 20,
color: queryType === "include" ? white : almostBlack,
backgroundColor:
queryType === "include"
? leafcutterElectricBlue
: white,
"&:hover": {
color: white,
backgroundColor: leafcutterElectricBlue,
},
}}
onClick={() => updateType("include")}
>
{t("include")}
</Button>
<Button
variant="contained"
color="primary"
size="small"
sx={{
fontSize: 10,
height: 20,
color: queryType === "exclude" ? white : almostBlack,
backgroundColor:
queryType === "exclude" ? warningPink : white,
"&:hover": {
color: white,
backgroundColor: warningPink,
},
}}
onClick={() => updateType("exclude")}
>
{t("exclude")}
</Button>
</ButtonGroup>
</Grid>
<Grid item>
<Box sx={{ ...small, mt: "0px" }}>these items:</Box>
</Grid>
</Grid>
) : null}
<Box>{children}</Box>
</AccordionDetails>
</Accordion>
</Grid>
);
};

View file

@ -1,118 +0,0 @@
"use client";
import { FC, useState, useEffect } from "react";
import { Box, Grid, TextField, Select, MenuItem } from "@mui/material";
import { DatePicker } from "@mui/x-date-pickers-pro";
import { useTranslate } from "react-polyglot";
import { useAppContext } from "./AppProvider";
interface QueryDateRangeSelectorProps {}
export const QueryDateRangeSelector: FC<QueryDateRangeSelectorProps> = () => {
const t = useTranslate();
const [relativeDate, setRelativeDate] = useState("");
const [startDate, setStartDate] = useState(null);
const [endDate, setEndDate] = useState(null);
const { updateQuery, query } = useAppContext();
useEffect(() => {
setStartDate(query.startDate.values[0] ?? null);
setEndDate(query.endDate.values[0] ?? null);
setRelativeDate(query.relativeDate.values[0] ?? "");
}, [query, setStartDate, setEndDate, setRelativeDate]);
return (
<Box sx={{ height: 305, width: "100%", pt: 2 }}>
<Grid container direction="column">
<Grid item xs={12} sx={{ mb: 2 }}>
<Select
fullWidth
size="small"
placeholder={t("relativeDate")}
value={relativeDate}
onChange={(event: any) => {
setStartDate(null);
setEndDate(null);
setRelativeDate(event.target.value);
updateQuery({
startDate: { values: [] },
});
updateQuery({
endDate: { values: [] },
});
updateQuery({
relativeDate: { values: [event.target.value] },
});
}}
>
<MenuItem value={7}>{t("last7Days")}</MenuItem>
<MenuItem value={30}>{t("last30Days")}</MenuItem>
<MenuItem value={90}>{t("last3Months")}</MenuItem>
<MenuItem value={180}>{t("last6Months")}</MenuItem>
<MenuItem value={365}>{t("lastYear")}</MenuItem>
<MenuItem value={730}>{t("last2Years")}</MenuItem>
</Select>
</Grid>
<Grid item sx={{ textAlign: "center", mb: 2 }}>
or
</Grid>
<Grid item xs={12}>
<DatePicker
label={t("startDate")}
value={startDate}
onChange={(date) => {
setStartDate(date);
updateQuery({
startDate: { values: [date] },
});
}}
// @ts-ignore
renderInput={(params) => (
<TextField
{...params}
sx={{
width: "100%",
color: "black",
"& .MuiOutlinedInput-root": {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
}}
size="small"
/>
)}
/>
</Grid>
<Grid item xs={12}>
<DatePicker
label={t("endDate")}
value={endDate}
onChange={(date) => {
setEndDate(date);
updateQuery({
endDate: { values: [date] },
});
}}
// @ts-ignore
renderInput={(params) => (
<TextField
{...params}
sx={{
backgroundColor: "white",
mt: "-1px",
width: "100%",
color: "black",
"& .MuiOutlinedInput-root": {
borderTop: 0,
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
},
}}
size="small"
/>
)}
/>
</Grid>
</Grid>
</Box>
);
};

View file

@ -1,94 +0,0 @@
"use client";
import { FC, useState, useEffect } from "react";
import { Box, Grid, Tooltip } from "@mui/material";
import { DataGridPro, GridColDef } from "@mui/x-data-grid-pro";
import { useAppContext } from "./AppProvider";
interface QueryListSelectorProps {
title: string;
keyName: string;
values: any;
width: number;
}
export const QueryListSelector: FC<QueryListSelectorProps> = ({
title,
keyName,
values,
width,
}) => {
const [selectionModel, setSelectionModel] = useState([] as any[]);
const {
colors: { leafcutterLightBlue, pink, leafcutterElectricBlue, warningPink },
typography: { small },
query,
updateQuery,
} = useAppContext();
const isExclude = query[keyName]?.queryType === "exclude";
const columns: GridColDef[] = [
{
field: "value",
renderHeader: () => (
<Box sx={{ ...small, fontWeight: "bold" }}>{title}</Box>
),
renderCell: ({ value, row }) => (
<Tooltip title={row.description}>
<Box sx={{ width: "100%" }}>{value}</Box>
</Tooltip>
),
editable: false,
flex: 1,
},
];
const rows = Object.keys(values).map((k) => ({
id: k,
value: values[k].display,
description: values[k].description,
category: values[k].category,
}));
useEffect(() => {
setSelectionModel(query[keyName].values);
}, [query, keyName, setSelectionModel]);
return (
<Grid item xs={width}>
<Box style={{ height: 280, width: "100%" }}>
<Grid container direction="column" spacing={2}>
<Grid item>
<DataGridPro
sx={{
height: 260,
"& .MuiCheckbox-root": {
color: isExclude ? warningPink : leafcutterElectricBlue,
},
"& .Mui-selected": {
backgroundColor: `${
isExclude ? pink : leafcutterLightBlue
} !important`,
},
}}
rows={rows}
columns={columns}
density="compact"
pageSizeOptions={[100]}
checkboxSelection
disableRowSelectionOnClick
hideFooter
disableColumnMenu
scrollbarSize={10}
onRowSelectionModelChange={(newSelectionModel) => {
setSelectionModel(newSelectionModel);
updateQuery({
[keyName]: { values: newSelectionModel },
});
}}
rowSelectionModel={selectionModel}
/>
</Grid>
</Grid>
</Box>
</Grid>
);
};

View file

@ -1,126 +0,0 @@
"use client";
import { FC, useState, useEffect } from "react";
import { Box, Grid } from "@mui/material";
import { useTranslate } from "react-polyglot";
import taxonomy from "app/_config/taxonomy.json";
import { colors } from "app/_styles/theme";
import { useAppContext } from "./AppProvider";
export const QueryText: FC = () => {
const t = useTranslate();
const {
typography: { h6 },
query: q,
} = useAppContext();
const displayNames: any = {
incidentType: t("incidentType"),
startDate: t("startDate"),
endDate: t("endDate"),
relativeDate: t("relativeDate"),
targetedGroup: t("targetedGroup"),
platform: t("platform"),
device: t("device"),
service: t("service"),
maker: t("maker"),
country: t("country"),
subregion: t("subregion"),
continent: t("continent"),
};
const createClause = (query: any, key: string) => {
const { values, queryType } = query[key];
const color =
queryType === "include"
? colors.leafcutterElectricBlue
: colors.warningPink;
if (values.length > 0) {
return `where <span style="color: ${color};"><strong>${
displayNames[key]
}</strong> ${
queryType === "include" ? ` ${t("is")} ` : ` ${t("isNot")} `
} ${values
.map(
// @ts-expect-error
(value: string) => `<em>${taxonomy[key]?.[value]?.display ?? ""}</em>`
)
.join(` ${t("or")} `)}</span>`;
}
return null;
};
const createDateClause = (query: any, key: string) => {
const { values } = query[key];
const color = colors.leafcutterElectricBlue;
if (values.length > 0) {
const range = key === "startDate" ? t("onOrAfter") : t("onOrBefore");
return `${t("where")} <span style="color: ${color};"><strong>${
displayNames[key]
}</strong> is ${range} <em>${values[0]?.toLocaleDateString()}</em></span>`;
}
return null;
};
const createRelativeDateClause = (query: any, key: string) => {
const { values } = query[key];
const color = colors.leafcutterElectricBlue;
if (query[key].values.length > 0) {
const range = t("onOrAfter");
return `${t("where")} <span style="color: ${color};"><strong>${
displayNames[key]
}</strong> is ${range} <em>${values[0]} days ago</em></span>`;
}
return null;
};
const [queryText, setQueryText] = useState(t("findAllIncidents"));
useEffect(() => {
const generateQueryText = (query: any) => {
const incidentClause = createClause(query, "incidentType");
const startDateClause = createDateClause(query, "startDate");
const endDateClause = createDateClause(query, "endDate");
const relativeDateClause = createRelativeDateClause(
query,
"relativeDate"
);
const targetedGroupClause = createClause(query, "targetedGroup");
const platformClause = createClause(query, "platform");
const deviceClause = createClause(query, "device");
const serviceClause = createClause(query, "service");
const makerClause = createClause(query, "maker");
const countryClause = createClause(query, "country");
const subregionClause = createClause(query, "subregion");
const continentClause = createClause(query, "continent");
const joinedClauses = [
incidentClause,
startDateClause,
endDateClause,
relativeDateClause,
targetedGroupClause,
platformClause,
deviceClause,
serviceClause,
makerClause,
countryClause,
subregionClause,
continentClause,
]
.filter((clause) => clause !== null)
.join(" and ");
return `${t("findAllIncidents")} ${joinedClauses}`;
};
const text = generateQueryText(q);
setQueryText(text);
}, [q]);
return (
<Grid container direction="column">
<Grid item>
<Box sx={h6} dangerouslySetInnerHTML={{ __html: queryText }} />
</Grid>
</Grid>
);
};

View file

@ -1,80 +0,0 @@
"use client";
import { FC, useState } from "react";
import ReactMarkdown from "react-markdown";
import {
Grid,
Box,
Accordion,
AccordionSummary,
AccordionDetails,
} from "@mui/material";
import {
ChevronRight as ChevronRightIcon,
ExpandMore as ExpandMoreIcon,
Circle as CircleIcon,
} from "@mui/icons-material";
import { useAppContext } from "./AppProvider";
interface QuestionProps {
question: string;
answer: string;
}
export const Question: FC<QuestionProps> = ({ question, answer }) => {
const [expanded, setExpanded] = useState(false);
const {
colors: { lavender, darkLavender },
typography: { h5, p },
} = useAppContext();
return (
<Accordion
expanded={expanded}
onChange={() => setExpanded(!expanded)}
elevation={0}
sx={{ "::before": { display: "none" } }}
>
<AccordionSummary>
<Grid
container
direction="row"
justifyContent="space-between"
sx={{ maxWidth: 500 }}
>
<Box component="h5" sx={h5}>
<CircleIcon
sx={{
fontSize: 14,
color: expanded ? darkLavender : lavender,
mr: 1,
mb: "-2px",
}}
/>
{question}
</Box>
{expanded ? (
<ExpandMoreIcon
htmlColor={lavender}
fontSize="medium"
sx={{ mt: "2px" }}
/>
) : (
<ChevronRightIcon
htmlColor={lavender}
fontSize="medium"
sx={{ mt: "4px" }}
/>
)}
</Grid>
</AccordionSummary>
<AccordionDetails sx={{ border: 0 }}>
<Box
sx={{ ...p, p: 2, border: `1px solid ${lavender}`, borderRadius: 3 }}
>
<ReactMarkdown>{answer}</ReactMarkdown>
</Box>
</AccordionDetails>
</Accordion>
);
};

View file

@ -1,86 +0,0 @@
"use client";
import { FC } from "react";
import { Box, Grid } from "@mui/material";
import { DataGridPro } from "@mui/x-data-grid-pro";
import { useTranslate } from "react-polyglot";
interface RawDataViewerProps {
rows: any[];
height: number;
}
export const RawDataViewer: FC<RawDataViewerProps> = ({ rows, height }) => {
const t = useTranslate();
const columns = [
{
field: "date",
headerName: t("date"),
editable: false,
flex: 0.7,
valueFormatter: ({ value }: any) => new Date(value).toLocaleDateString(),
},
{
field: "incident",
headerName: t("incident"),
editable: false,
flex: 1,
},
{
field: "technology",
headerName: t("technology"),
editable: false,
flex: 0.8,
},
{
field: "targeted_group",
headerName: t("targetedGroup"),
editable: false,
flex: 1.3,
},
{
field: "country",
headerName: t("country"),
editable: false,
flex: 1,
},
{
field: "region",
headerName: t("subregion"),
editable: false,
flex: 1,
},
{
field: "continent",
headerName: t("continent"),
editable: false,
flex: 1,
},
];
return (
<Grid item xs={12}>
<Box
sx={{ width: "100%", height }}
onClick={(e: any) => e.stopPropagation()}
>
<Grid container direction="column" spacing={2}>
<Grid item>
<DataGridPro
sx={{ width: "100%", height }}
rows={rows}
columns={columns}
density="compact"
pageSizeOptions={[100]}
disableRowSelectionOnClick
hideFooter
disableColumnMenu
scrollbarSize={10}
disableVirtualization
/>
</Grid>
</Grid>
</Box>
</Grid>
);
};

View file

@ -20,8 +20,8 @@ import {
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useTranslate } from "react-polyglot";
import { useAppContext } from "app/_components/AppProvider";
import { Tooltip } from "app/_components/Tooltip";
import { useAppContext } from "./AppProvider";
import { Tooltip } from "leafcutter-common";
// import { ArrowCircleRight as ArrowCircleRightIcon } from "@mui/icons-material";
const MenuItem = ({

View file

@ -1,160 +0,0 @@
"use client";
/* eslint-disable react/require-default-props */
import { FC } from "react";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import {
Box,
Grid,
Tooltip as MUITooltip,
Button,
IconButton,
} from "@mui/material";
import { Close as CloseIcon } from "@mui/icons-material";
import { useTranslate } from "react-polyglot";
import { useAppContext } from "./AppProvider";
interface TooltipProps {
title: string;
description: string;
placement: any;
tooltipID: string;
nextURL?: string;
previousURL?: string;
children: any;
}
export const Tooltip: FC<TooltipProps> = ({
title,
description,
placement,
tooltipID,
children,
previousURL = null,
nextURL = null,
// eslint-disable-next-line arrow-body-style
}) => {
const t = useTranslate();
const {
typography: { p, small },
colors: { white, leafcutterElectricBlue, almostBlack },
} = useAppContext();
const router = useRouter();
const pathname = usePathname() ?? "";
const searchParams = useSearchParams();
const activeTooltip = searchParams?.get("tooltip")?.toString();
const open = activeTooltip === tooltipID;
const showNavigation = true;
return (
<MUITooltip
open={open}
title={
<Grid container direction="column">
<Grid item container direction="row-reverse">
<Grid item>
<IconButton onClick={() => router.push(pathname)}>
<CloseIcon
sx={{
color: leafcutterElectricBlue,
fontSize: "14px",
mt: 1,
}}
/>
</IconButton>
</Grid>
</Grid>
<Grid item>
<Box sx={{ p: "12px", pt: 0, mb: "6px" }}>
<Box sx={{ ...p, fontWeight: "bold" }}>
<Grid container direction="row" alignItems="center">
<Grid item>{title}</Grid>
</Grid>
</Box>
<Box sx={{ ...small, mt: 1, color: almostBlack }}>
{description}
</Box>
</Box>
</Grid>
{showNavigation ? (
<Grid
item
container
direction="row"
justifyContent="space-between"
alignItems="center"
sx={{ p: "12px" }}
>
<Grid item>
{previousURL ? (
<Button
sx={{
...small,
borderRadius: 500,
border: `1px solid ${leafcutterElectricBlue}`,
p: "2px 8px",
color: leafcutterElectricBlue,
textTransform: "none",
}}
onClick={() => router.push(previousURL)}
>
{t("previous")}
</Button>
) : null}
</Grid>
<Grid item>
{nextURL ? (
<Button
sx={{
...small,
borderRadius: 500,
border: `1px solid ${leafcutterElectricBlue}`,
p: "2px 8px",
color: leafcutterElectricBlue,
textTransform: "none",
}}
onClick={() => router.push(nextURL)}
>
{t("next")}
</Button>
) : (
<Button
sx={{
...small,
borderRadius: 500,
border: `1px solid ${leafcutterElectricBlue}`,
p: "2px 8px",
color: leafcutterElectricBlue,
textTransform: "none",
}}
onClick={() => router.push(pathname)}
>
{t("done")}
</Button>
)}
</Grid>
</Grid>
) : null}
</Grid>
}
arrow
placement={placement}
sx={{ opacity: 0.9 }}
componentsProps={{
tooltip: {
sx: {
opacity: 1.0,
backgroundColor: white,
color: leafcutterElectricBlue,
boxShadow: "0px 6px 20px rgba(0,0,0,0.25)",
},
},
arrow: {
sx: { opacity: 1.0, fontSize: "22px", color: white },
},
}}
>
{children}
</MUITooltip>
);
};

View file

@ -6,9 +6,9 @@ import Image from "next/legacy/image";
import { AppBar, Grid, Box } from "@mui/material";
import { useTranslate } from "react-polyglot";
import LeafcutterLogo from "images/leafcutter-logo.png";
import { AccountButton } from "app/_components/AccountButton";
import { HelpButton } from "app/_components/HelpButton";
import { Tooltip } from "app/_components/Tooltip";
import { AccountButton } from "./AccountButton";
import { HelpButton } from "./HelpButton";
import { Tooltip } from "leafcutter-common";
import { useAppContext } from "./AppProvider";
// import { LanguageSelect } from "./LanguageSelect";
@ -43,50 +43,51 @@ export const TopNav: FC = () => {
wrap="nowrap"
spacing={4}
>
<Link href="/" passHref>
<Grid
item
container
direction="row"
justifyContent="flex-start"
spacing={1}
wrap="nowrap"
sx={{ cursor: "pointer" }}
>
<Grid item sx={{ pr: 1 }}>
<Image src={LeafcutterLogo} alt="" width={56} height={52} />
<Grid
item
container
direction="row"
justifyContent="flex-start"
alignItems="center"
alignContent="center"
spacing={1}
wrap="nowrap"
sx={{ cursor: "pointer" }}
>
<Grid item sx={{ pr: 1 }}>
<Image src={LeafcutterLogo} alt="" width={56} height={52} />
</Grid>
<Grid item container direction="column" alignContent="flex-start">
<Grid item sx={{ mt: -1 }}>
<Box
sx={{
...h5,
color: leafcutterElectricBlue,
p: 0,
m: 0,
pt: 1,
textAlign: "left",
}}
>
Leafcutter
</Box>
</Grid>
<Grid item container direction="column" alignContent="flex-start">
<Grid item>
<Box
sx={{
...h5,
color: leafcutterElectricBlue,
p: 0,
m: 0,
pt: 1,
textAlign: "left",
}}
>
Leafcutter
</Box>
</Grid>
<Grid item>
<Box
sx={{
...h6,
m: 0,
p: 0,
color: cdrLinkOrange,
textAlign: "left",
}}
>
A Project of Center for Digital Resilience
</Box>
</Grid>
<Grid item>
<Box
sx={{
...h6,
m: 0,
p: 0,
color: cdrLinkOrange,
textAlign: "left",
}}
>
A Project of Center for Digital Resilience
</Box>
</Grid>
</Grid>
</Link>
</Grid>
<Grid item>
<HelpButton />
</Grid>

View file

@ -1,373 +0,0 @@
"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>
);
};

View file

@ -1,75 +0,0 @@
"use client";
import { FC, useState } from "react";
import { Grid, Card, Box } from "@mui/material";
import Iframe from "react-iframe";
import { useAppContext } from "app/_components/AppProvider";
import { VisualizationDetailDialog } from "app/_components/VisualizationDetailDialog";
interface VisualizationCardProps {
id: string;
title: string;
description: string;
url: string;
}
export const VisualizationCard: FC<VisualizationCardProps> = ({
id,
title,
description,
url,
}) => {
const [open, setOpen] = useState(false);
const closeDialog = () => setOpen(false);
const {
typography: { h4, p },
colors: { leafcutterLightBlue, leafcutterElectricBlue },
} = useAppContext();
const finalURL = `${process.env.NEXT_PUBLIC_NEXTAUTH_URL}${url}&_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!t%2Cvalue%3A0)%2Ctime%3A(from%3Anow-3y%2Cto%3Anow))`;
return (
<>
<Grid item xs={6}>
<Card
elevation={0}
sx={{
border: `1px solid ${leafcutterElectricBlue}`,
borderRadius: "10px",
backgroundColor: leafcutterLightBlue,
p: 2,
cursor: "pointer",
}}
onClick={() => setOpen(true)}
>
<Box
sx={{
backgroundColor: leafcutterLightBlue,
pointerEvents: "none",
borderRadius: "8px",
overflow: "hidden",
p: 1,
}}
>
<Iframe url={finalURL} height="300" width="100%" frameBorder={0} />
</Box>
<Box component="h4" sx={{ ...h4, mt: 2, mb: 2 }}>
{title}
</Box>
<Box component="p" sx={{ ...p, mt: 2, mb: 2 }}>
{description}
</Box>
</Card>
</Grid>
{open ? (
<VisualizationDetailDialog
id={id}
title={title}
description={description}
url={url}
closeDialog={closeDialog}
editing={false}
/>
) : null}
</>
);
};

View file

@ -1,45 +0,0 @@
"use client";
import { FC } from "react";
// import Link from "next/link";
import { Box } from "@mui/material";
import Iframe from "react-iframe";
import { useAppContext } from "app/_components/AppProvider";
interface VisualizationDetailProps {
id: string;
title: string;
description: string;
url: string;
editing: boolean;
}
export const VisualizationDetail: FC<VisualizationDetailProps> = ({
id,
title,
description,
url,
editing,
}) => {
const {
colors: { mediumGray },
typography: { h4, p },
} = useAppContext();
const finalURL = `${process.env.NEXT_PUBLIC_NEXTAUTH_URL}${url}&_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!t%2Cvalue%3A0)%2Ctime%3A(from%3Anow-3y%2Cto%3Anow))`;
console.log({ finalURL });
return (
<Box key={id}>
{!editing ? (
<Box sx={{ borderBottom: `1px solid ${mediumGray}`, mb: 2 }}>
<Box sx={{ ...h4, mt: 1, mb: 1 }}>{title}</Box>
<Box sx={{ ...p, mt: 0, mb: 2, fontStyle: "oblique" }}>
{description}
</Box>
</Box>
) : null}
<Box sx={{ borderBottom: `1px solid ${mediumGray}`, pb: 3 }}>
<Iframe url={finalURL} height="500px" width="100%" frameBorder={0} />
</Box>
</Box>
);
};

View file

@ -1,157 +0,0 @@
"use client";
import { FC, useState } from "react";
// import Link from "next/link";
import {
Grid,
Button,
Dialog,
DialogActions,
DialogContent,
TextField,
} from "@mui/material";
import { useTranslate } from "react-polyglot";
import { useAppContext } from "app/_components/AppProvider";
import { VisualizationDetail } from "./VisualizationDetail";
interface VisualizationDetailDialogProps {
id: string;
title: string;
description: string;
url: string;
closeDialog: any;
editing: boolean;
}
export const VisualizationDetailDialog: FC<VisualizationDetailDialogProps> = ({
id,
title,
description,
url,
closeDialog,
editing,
}) => {
const t = useTranslate();
const [editedTitle, setEditedTitle] = useState(title);
const [editedDescription, setEditedDescription] = useState(description);
const {
colors: { leafcutterElectricBlue, leafcutterLightBlue, white, almostBlack },
query,
} = useAppContext();
const deleteAndClose = async () => {
await fetch(`/api/visualizations/delete`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ id }),
});
closeDialog();
};
const saveAndClose = async () => {
const updateParams = {
id,
title: editedTitle,
description: editedDescription,
query,
};
await fetch(`/api/visualizations/update`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(updateParams),
});
closeDialog();
};
const buttonStyles = {
fontSize: 14,
borderRadius: 500,
color: white,
backgroundColor: leafcutterElectricBlue,
fontWeight: "bold",
textTransform: "uppercase",
pl: 3,
pr: 3,
":hover": {
backgroundColor: leafcutterLightBlue,
color: almostBlack,
opacity: 0.8,
},
};
return (
<Dialog open maxWidth="xl">
<DialogContent sx={{ minWidth: 800 }}>
{editing && (
<Grid direction="column" container rowGap={2} sx={{ mb: 3 }}>
<Grid item>
<TextField
value={editedTitle}
onChange={(e) => setEditedTitle(e.target.value)}
label={t("title")}
size="small"
fullWidth
/>
</Grid>
<Grid>
<TextField
value={editedDescription}
onChange={(e) => setEditedDescription(e.target.value)}
label={t("description")}
size="small"
fullWidth
/>
</Grid>
</Grid>
)}
<VisualizationDetail
id={id}
title={title}
description={description}
url={url}
editing={editing}
/>
</DialogContent>
<DialogActions sx={{ p: 2.5, pt: 0 }}>
<Grid container direction="row-reverse" justifyContent="space-between">
{!editing && (
<Grid item>
<Button sx={buttonStyles} onClick={closeDialog} size="small">
{t("done")}
</Button>
</Grid>
)}
{!editing && (
<Grid item>
<Button sx={buttonStyles} onClick={deleteAndClose} size="small">
{t("delete")}
</Button>
</Grid>
)}
{editing && (
<Grid item>
<Button sx={buttonStyles} onClick={saveAndClose} size="small">
{t("save")}
</Button>
</Grid>
)}
{editing && (
<Grid item>
<Button sx={buttonStyles} onClick={deleteAndClose} size="small">
{t("cancel")}
</Button>
</Grid>
)}
</Grid>
</DialogActions>
</Dialog>
);
};

View file

@ -1,110 +0,0 @@
"use client";
import { FC } from "react";
import Image from "next/legacy/image";
import { Card, Grid } from "@mui/material";
import horizontalBar from "images/horizontal-bar.svg";
import horizontalBarStacked from "images/horizontal-bar-stacked.svg";
import verticalBar from "images/vertical-bar.svg";
import verticalBarStacked from "images/vertical-bar-stacked.svg";
import pieDonut from "images/pie-donut.svg";
import line from "images/line.svg";
import lineStacked from "images/line-stacked.svg";
import dataTable from "images/data-table.svg";
import metric from "images/metric.svg";
import tagCloud from "images/tag-cloud.svg";
import { useAppContext } from "./AppProvider";
interface VisualizationSelectCardProps {
visualizationType: string;
title: string;
enabled: boolean;
selected: boolean;
toggleSelected: any;
}
export const VisualizationSelectCard: FC<VisualizationSelectCardProps> = ({
visualizationType,
title,
enabled,
selected,
toggleSelected,
}) => {
const {
typography: { small },
colors: {
white,
leafcutterElectricBlue,
leafcutterLightBlue,
cdrLinkOrange,
},
} = useAppContext();
const images: any = {
horizontalBar,
horizontalBarStacked,
verticalBar,
verticalBarStacked,
line,
lineStacked,
pieDonut,
dataTable,
metric,
tagCloud,
unknown: line,
};
let backgroundColor = leafcutterElectricBlue;
if (!enabled) {
backgroundColor = leafcutterLightBlue;
} else if (selected) {
backgroundColor = cdrLinkOrange;
}
return (
<Card
sx={{
height: "100px",
backgroundColor,
borderRadius: "10px",
padding: "10px",
opacity: enabled ? 1 : 0.5,
cursor: enabled ? "pointer" : "default",
"&:hover": {
backgroundColor: enabled ? cdrLinkOrange : white,
},
}}
elevation={enabled ? 2 : 0}
onClick={() => toggleSelected(visualizationType)}
>
<Grid
direction="column"
container
justifyContent="space-around"
alignContent="center"
alignItems="center"
wrap="nowrap"
sx={{ height: "100%" }}
spacing={0}
>
<Grid
item
sx={{
...small,
textAlign: "center",
color: enabled ? white : leafcutterElectricBlue,
}}
>
{title}
</Grid>
<Grid item>
<Image
src={images[visualizationType]}
alt=""
width={35}
height={35}
/>
</Grid>
</Grid>
</Card>
);
};

View file

@ -1,58 +0,0 @@
"use client";
import { Box, Grid } from "@mui/material";
import { useSession } from "next-auth/react";
import { useTranslate } from "react-polyglot";
import { useAppContext } from "./AppProvider";
export const Welcome = () => {
const t = useTranslate();
const { data: session } = useSession();
/*
const {
user: { name },
} = session as any;
*/
const name = "Test User";
const {
colors: { white, leafcutterElectricBlue },
typography: { h1, h4, p },
} = useAppContext();
return (
<Box
sx={{
width: "100%",
backgroundColor: leafcutterElectricBlue,
color: white,
p: 4,
borderRadius: "10px",
mb: "22px",
}}
>
<Grid container direction="row" spacing={3}>
{/* <Grid
item
container
xs={3}
direction="column"
justifyContent="flex-start"
alignItems="center"
>
<img src={image} alt={name} width="150px" />
</Grid> */}
<Grid item xs={12}>
<Box component="h1" sx={{ ...h1, mb: 1 }}>
{t("dashboardTitle")}
</Box>
<Box component="h4" sx={{ ...h4, mt: 1, mb: 1 }}>{`${t("welcome")}, ${
name?.split(" ")[0]
}! 👋`}</Box>
<Box component="p" sx={{ ...p }}>
{t("dashboardDescription")}
</Box>
</Grid>
</Grid>
</Box>
);
};

View file

@ -1,140 +0,0 @@
"use client";
import { Box, Grid, Dialog, Button } from "@mui/material";
import { useRouter, useSearchParams } from "next/navigation";
// import { useSession } from "next-auth/react";
// import { useTranslate } from "react-polyglot";
import { useAppContext } from "./AppProvider";
export const WelcomeDialog = () => {
// const t = useTranslate();
const router = useRouter();
const searchParams = useSearchParams();
// const { data: session } = useSession();
// const { user } = session;
const {
colors: { white, leafcutterElectricBlue },
typography: { h1, h6, p },
} = useAppContext();
const activeTooltip = searchParams?.get("tooltip")?.toString();
const open = activeTooltip === "welcome";
return (
<Dialog open={open} maxWidth="md" sx={{ zIndex: 2000 }}>
<Box sx={{ p: 6, pt: 6 }}>
<Grid container direction="column" spacing={3}>
<Grid item container direction="row" justifyContent="center">
<Grid
item
sx={{ width: 500, height: 300, backgroundColor: "black" }}
>
{/* <iframe
width="500"
height="300"
src="https://www.youtube-nocookie.com/embed/-iKFBXAlmEM"
title="CDR Link intro"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/> */}
</Grid>
</Grid>
<Grid item>
<Box
sx={{
...h1,
color: leafcutterElectricBlue,
textAlign: "center",
fontSize: 32,
}}
>
Welcome to Leafcutter!
</Box>
<Box
sx={{ ...h6, color: leafcutterElectricBlue, pt: 1, fontSize: 16 }}
>
Let&apos;s get started.
</Box>
</Grid>
<Grid item container spacing={3}>
<Grid item>
<Box sx={{ ...p, textAlign: "center" }}>
Leafcutter is a secure platform for aggregating, displaying, and
sharing data on digital security threats and attacks facing
global civil society. When creating the app we had a couple of
people in mind; Incident responders, threat analysts, security
trainers, and security service providers.
</Box>
</Grid>
<Grid item>
<Box sx={{ ...p, textAlign: "center" }}>
Leafcutter onboarding is meant to help you navigate, build
queries and visualizations, and walk you through the best ways
to know and use the Leafcutter app. Ready?
</Box>
</Grid>
</Grid>
<Grid
item
container
direction="row"
justifyContent="space-around"
sx={{ mt: 3 }}
>
<Grid item>
<Button
sx={{
fontSize: 14,
minWidth: 300,
borderRadius: 500,
color: leafcutterElectricBlue,
border: `2px solid ${leafcutterElectricBlue}`,
fontWeight: "bold",
textTransform: "uppercase",
pl: 6,
pr: 5,
":hover": {
backgroundColor: leafcutterElectricBlue,
color: white,
opacity: 0.8,
},
}}
onClick={() => {
router.push(`/`);
}}
>
I&apos;ll explore on my own
</Button>
</Grid>
<Grid item>
<Button
sx={{
fontSize: 14,
minWidth: 300,
borderRadius: 500,
backgroundColor: leafcutterElectricBlue,
color: white,
border: `2px solid ${leafcutterElectricBlue}`,
fontWeight: "bold",
textTransform: "uppercase",
pl: 6,
pr: 5,
":hover": {
backgroundColor: leafcutterElectricBlue,
color: white,
opacity: 0.8,
},
}}
onClick={() => {
router.push(`/?tooltip=navigation`);
}}
>
Start the guide
</Button>
</Grid>
</Grid>
</Grid>
</Box>
</Dialog>
);
};

View file

@ -1,8 +1,15 @@
import type { NextAuthOptions } from "next-auth";
import Google from "next-auth/providers/google";
import Apple from "next-auth/providers/apple";
import Credentials from "next-auth/providers/credentials";
import { checkAuth } from "./opensearch";
export const authOptions: NextAuthOptions = {
pages: {
signIn: "/login",
error: "/login",
signOut: "/logout",
},
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID ?? "",
@ -12,6 +19,62 @@ export const authOptions: NextAuthOptions = {
clientId: process.env.APPLE_CLIENT_ID ?? "",
clientSecret: process.env.APPLE_CLIENT_SECRET ?? "",
}),
Credentials({
name: "Link",
credentials: {
authToken: { label: "AuthToken", type: "text", },
},
async authorize(credentials, req) {
const { headers } = req;
console.log({ headers });
const leafcutterUser = headers?.["x-leafcutter-user"];
const authToken = credentials?.authToken;
if (!leafcutterUser || leafcutterUser.trim() === "") {
console.log("no leafcutter user");
return null;
}
console.log({ authToken });
return null;
/*
try {
// add role check
await checkAuth(username, password);
const user = {
id: leafcutterUser,
email: leafcutterUser
};
return user;
} catch (e) {
console.log({ e });
}
return null;
*/
}
})
],
secret: process.env.NEXTAUTH_SECRET,
/*
callbacks: {
signIn: async ({ user, account, profile }) => {
const roles: any = [];
return roles.includes("admin") || roles.includes("agent");
},
session: async ({ session, user, token }) => {
// @ts-ignore
session.user.roles = token.roles;
return session;
},
jwt: async ({ token, user, account, profile, trigger }) => {
if (user) {
token.roles = [];
}
return token;
}
},*/
};

View file

@ -23,6 +23,24 @@ const createClient = () => new Client({
},
});
const createUserClient = (username: string, password: string) => new Client({
node: baseURL,
auth: {
username,
password,
},
ssl: {
rejectUnauthorized: false,
},
});
export const checkAuth = async (username: string, password: string) => {
const client = createUserClient(username, password);
const res = await client.ping();
return res.statusCode === 200;
};
const getDocumentID = (doc: any) => doc._id.split(":")[1];
const getEmbedURL = (tenant: string, visualizationID: string) =>

View file

@ -3,3 +3,7 @@ body {
overscroll-behavior-y: none;
text-size-adjust: none;
}
a {
text-decoration: none;
}

View file

@ -0,0 +1,15 @@
import { NextRequest, NextResponse } from "next/server";
export const GET = async (req: NextRequest) => {
const validDomains = "localhost";
console.log({ req });
return NextResponse.json({ response: "ok" });
};
export const POST = async (req: NextRequest) => {
const validDomains = "localhost";
console.log({ req });
return NextResponse.json({ response: "ok" });
};

View file

@ -1,38 +0,0 @@
import { createProxyMiddleware } from "http-proxy-middleware";
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
const withAuthInfo =
(handler: any) => async (req: NextApiRequest, res: NextApiResponse) => {
const session: any = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
req.headers["x-proxy-user"] = session.email.toLowerCase();
req.headers["x-proxy-roles"] = "leafcutter_user";
const auth = `${session.email.toLowerCase()}:${process.env.OPENSEARCH_USER_PASSWORD}`;
const buff = Buffer.from(auth);
const base64data = buff.toString("base64");
req.headers.Authorization = `Basic ${base64data}`;
return handler(req, res);
};
const proxy = createProxyMiddleware({
target: process.env.OPENSEARCH_DASHBOARDS_URL,
changeOrigin: true,
xfwd: true,
});
export default withAuthInfo(proxy);
export const config = {
api: {
bodyParser: false,
externalResolver: true,
},
};

View file

@ -3,7 +3,9 @@ import { getTrends } from "app/_lib/opensearch";
export const GET = async () => {
const results = await getTrends(5);
console.log({ results });
NextResponse.json(results);
};
export const dynamic = 'force-dynamic';

View file

@ -0,0 +1,13 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "app/_lib/auth";
import { getUserVisualizations } from "app/_lib/opensearch";
export const GET = async () => {
const session = await getServerSession(authOptions);
const { user: { email } }: any = session;
const visualizations = await getUserVisualizations(email, 20);
return NextResponse.json(visualizations);
};

View file

@ -8,7 +8,7 @@ import "@fontsource/roboto/700.css";
import "@fontsource/playfair-display/900.css";
// import getConfig from "next/config";
// import { LicenseInfo } from "@mui/x-data-grid-pro";
import { MultiProvider } from "app/_components/MultiProvider";
import { MultiProvider } from "./_components/MultiProvider";
export const metadata: Metadata = {
title: "Leafcutter",

View file

@ -2,5 +2,5 @@ apiVersion: v2
name: leafcutter
description: A Helm chart for Kubernetes
type: application
version: 0.1.54
appVersion: "0.1.54"
version: 0.2.0
appVersion: "0.2.0"

View file

@ -1,57 +1,6 @@
import { NextResponse } from "next/server";
import { withAuth, NextRequestWithAuth } from "next-auth/middleware";
import getConfig from "next/config";
const rewriteURL = (request: NextRequestWithAuth, originBaseURL: string, destinationBaseURL: string, headers: any = {}) => {
if (request.nextUrl.protocol.startsWith('ws')) {
return NextResponse.next();
}
if (request.nextUrl.pathname.includes('/_next/static/development/')) {
return NextResponse.next();
}
const destinationURL = request.url.replace(originBaseURL, destinationBaseURL);
console.log(`Rewriting ${request.url} to ${destinationURL}`);
const requestHeaders = new Headers(request.headers);
for (const [key, value] of Object.entries(headers)) {
// @ts-ignore
requestHeaders.set(key, value);
}
requestHeaders.delete('connection');
// console.log({ finalHeaders: requestHeaders });
return NextResponse.rewrite(new URL(destinationURL), { request: { headers: requestHeaders } });
};
const checkRewrites = async (request: NextRequestWithAuth) => {
console.log({ currentURL: request.nextUrl.href });
const leafcutterBaseURL = process.env.LEAFCUTTER_URL ?? "http://localhost:3000";
const opensearchDashboardsURL = process.env.OPENSEARCH_URL ?? "http://localhost:5602";
if (request.nextUrl.pathname.startsWith('/proxy/opensearch')) {
console.log('proxying to zammad');
const { token } = request.nextauth;
const auth = `${token?.email?.toLowerCase()}:${process.env.OPENSEARCH_USER_PASSWORD}`;
const buff = Buffer.from(auth);
const base64data = buff.toString("base64");
const headers = {
'X-Proxy-User': token?.email?.toLowerCase(),
"X-Proxy-Roles": "leafcutter_user",
"Authorization": `Basic ${base64data}`
};
console.log({ headers });
return rewriteURL(request, `${leafcutterBaseURL}/proxy/opensearch`, opensearchDashboardsURL, headers);
}
};
import { withAuth } from "next-auth/middleware";
export default withAuth(
checkRewrites,
{
pages: {
signIn: `/login`,
@ -60,25 +9,30 @@ export default withAuth(
authorized: ({ token, req }) => {
const {
url,
headers,
} = req;
// check login page
const parsedURL = new URL(url);
if (parsedURL.pathname.startsWith('/login')) {
console.log({ url });
console.log({ pathname: parsedURL.pathname });
console.log({ allowed: parsedURL.pathname.startsWith("/app") });
const allowed = parsedURL.pathname.startsWith('/login') || parsedURL.pathname.startsWith('/api' || parsedURL.pathname.startsWith("/app"));
if (allowed) {
return true;
}
// check session auth
const authorizedDomains = ["redaranj.com", "digiresilience.org"];
const userDomain = token?.email?.toLowerCase().split("@").pop() ?? "unauthorized.net";
if (authorizedDomains.includes(userDomain)) {
if (token?.email) {
return true;
}
return false;
},
}
}
);
export const config = {
matcher: [
'/((?!api|app|bootstrap|3961|ui|translations|internal|login|node_modules|_next/static|_next/image|favicon.ico).*)',
],
};

View file

@ -7,9 +7,16 @@ const ContentSecurityPolicy = `
`;
module.exports = {
publicRuntimeConfig: {
embedded: true
},/*
transpilePackages: ["leafcutter-common"],
rewrites: async () => ({
fallback: [
{
source: "/:path*",
destination: "/api/proxy/:path*",
},
],
}),
/*
basePath: "/proxy/leafcutter",
assetPrefix: "/proxy/leafcutter",
i18n: {
@ -17,25 +24,19 @@ module.exports = {
defaultLocale: "en",
},
*/
/* rewrites: async () => ({
fallback: [
{
source: "/:path*",
destination: "/api/proxy/:path*",
},
],
}) */
/*
async headers() {
return [
{
source: '/:path*',
headers: [
/*
{
key: 'Content-Security-Policy',
value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
},
*/
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
@ -52,4 +53,5 @@ module.exports = {
},
]
}
*/
};

View file

@ -6,7 +6,7 @@
"login": "aws sso login --sso-session cdr",
"kubeconfig": "aws eks update-kubeconfig --name cdr-leafcutter-dashboard-cluster --profile cdr-leafcutter-dashboard-production",
"fwd:opensearch": "kubectl port-forward opensearch-cluster-master-0 9200:9200 --namespace leafcutter",
"fwd:dashboards": "kubectl port-forward opensearch-dashboards-1-59854cdb9b-vgmtf 5602:5601 --namespace leafcutter",
"fwd:dashboards": "kubectl port-forward opensearch-dashboards-1-59854cdb9b-mx4qq 5602:5601 --namespace leafcutter",
"build": "next build",
"start": "next start",
"export": "next export",
@ -21,44 +21,46 @@
"@fontsource/poppins": "^5.0.8",
"@fontsource/roboto": "^5.0.8",
"@mui/icons-material": "^5",
"@mui/lab": "^5.0.0-alpha.138",
"@mui/lab": "^5.0.0-alpha.140",
"@mui/material": "^5",
"@mui/x-data-grid-pro": "^6.11.0",
"@mui/x-date-pickers-pro": "^6.11.0",
"@mui/x-data-grid-pro": "^6.11.1",
"@mui/x-date-pickers-pro": "^6.11.1",
"@opensearch-project/opensearch": "^2.3.1",
"cryptr": "^6.2.0",
"date-fns": "^2.30.0",
"http-proxy-middleware": "^2.0.6",
"leafcutter-common": "*",
"material-ui-popup-state": "^5.0.9",
"next": "13.4.13",
"next-auth": "^4.22.4",
"next": "13.4.7",
"next-auth": "^4.23.1",
"next-http-proxy-middleware": "^1.2.5",
"nodemailer": "^6.9.4",
"react": "18.2.0",
"react-cookie": "^4.1.1",
"react-cookie": "^5.0.0",
"react-cookie-consent": "^8.0.1",
"react-dom": "18.2.0",
"react-iframe": "^1.8.5",
"react-markdown": "^8.0.7",
"react-polyglot": "^0.7.2",
"sharp": "^0.32.4",
"swr": "^2.2.0",
"sharp": "^0.32.5",
"swr": "^2.2.1",
"tss-react": "^4.8.8",
"uuid": "^9.0.0"
},
"devDependencies": {
"@babel/core": "^7.22.9",
"@types/node": "^20.4.8",
"@types/react": "18.2.18",
"@babel/core": "^7.22.10",
"@types/node": "^20.5.0",
"@types/react": "18.2.20",
"@types/uuid": "^9.0.2",
"babel-loader": "^9.1.3",
"eslint": "^8.46.0",
"eslint": "^8.47.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-next": "^13.4.13",
"eslint-config-next": "^13.4.16",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.28.0",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "^7.33.1",
"eslint-plugin-react": "^7.33.2",
"typescript": "5.1.6"
}
}

View file

@ -0,0 +1,66 @@
import { createProxyMiddleware } from "http-proxy-middleware";
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
/*
if (validDomains.includes(domain)) {
res.headers.set("Access-Control-Allow-Origin", origin);
res.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.headers.set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
}
*/
const withAuthInfo =
(handler: any) => async (req: NextApiRequest, res: NextApiResponse) => {
const session: any = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
let email = session?.email?.toLowerCase();
const requestSignature = req.query.signature;
const url = new URL(req.headers.referer as string);
const referrerSignature = url.searchParams.get("signature");
console.log({ requestSignature, referrerSignature });
const isAppPath = !!req.url?.startsWith("/app");
const isResourcePath = !!req.url?.match(/\/(api|app|bootstrap|3961|ui|translations|internal|login|node_modules)/);
if (requestSignature && isAppPath) {
console.log("Has Signature");
}
if (referrerSignature && isResourcePath) {
console.log("Has Signature");
}
if (!email) {
return res.status(401).json({ error: "Not authorized" });
}
req.headers["x-proxy-user"] = email;
req.headers["x-proxy-roles"] = "leafcutter_user";
const auth = `${email}:${process.env.OPENSEARCH_USER_PASSWORD}`;
const buff = Buffer.from(auth);
const base64data = buff.toString("base64");
req.headers.Authorization = `Basic ${base64data}`;
return handler(req, res);
};
const proxy = createProxyMiddleware({
target: process.env.OPENSEARCH_DASHBOARDS_URL,
changeOrigin: true,
xfwd: true,
});
export default withAuthInfo(proxy);
export const config = {
api: {
bodyParser: false,
externalResolver: true,
},
};

View file

@ -24,6 +24,17 @@
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"../../packages/leafcutter-common/components/Create.tsx",
"../../packages/leafcutter-common/components/About.tsx",
"../../packages/leafcutter-common/components/FAQ.tsx",
"../../packages/leafcutter-common/components/Trends.tsx",
"../../packages/leafcutter-common/components/Preview.tsx",
"app/(main)/setup/_components/Setup.tsx"
, "app/api/proxy/[[...path]]" ],
"exclude": ["node_modules", "babel__core"]
}

View file

@ -1,9 +1,24 @@
"use client";
import { FC } from "react";
import { Box, Grid, Container, IconButton } from "@mui/material";
import { Apple as AppleIcon, Google as GoogleIcon } from "@mui/icons-material";
import { FC, useState } from "react";
import {
Box,
Grid,
Container,
IconButton,
Typography,
TextField,
} from "@mui/material";
import {
Apple as AppleIcon,
Google as GoogleIcon,
Key as KeyIcon,
} from "@mui/icons-material";
import { signIn } from "next-auth/react";
import Image from "next/image";
import LinkLogo from "public/link-logo-small.png";
import { colors } from "app/_styles/theme";
import { useSearchParams } from "next/navigation";
type LoginProps = {
session: any;
@ -14,62 +29,198 @@ export const Login: FC<LoginProps> = ({ session }) => {
typeof window !== "undefined" && window.location.origin
? window.location.origin
: "";
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const params = useSearchParams();
const error = params.get("error");
const { darkGray, cdrLinkOrange, white } = colors;
const buttonStyles = {
borderRadius: 500,
width: "100%",
fontSize: "16px",
fontWeight: "bold",
backgroundColor: white,
"&:hover": {
color: white,
backgroundColor: cdrLinkOrange,
},
};
const fieldStyles = {
"& label.Mui-focused": {
color: cdrLinkOrange,
},
"& .MuiInput-underline:after": {
borderBottomColor: cdrLinkOrange,
},
"& .MuiFilledInput-underline:after": {
borderBottomColor: cdrLinkOrange,
},
"& .MuiOutlinedInput-root": {
"&.Mui-focused fieldset": {
borderColor: cdrLinkOrange,
},
},
};
return (
<>
<Grid container direction="row-reverse" sx={{ p: 3 }}>
<Grid item />
</Grid>
<Container maxWidth="md" sx={{ mt: 3, mb: 20 }}>
<Box sx={{ backgroundColor: darkGray, height: "100vh" }}>
<Container maxWidth="md" sx={{ p: 10 }}>
<Grid container spacing={2} direction="column" alignItems="center">
<Grid item>
<Box sx={{ maxWidth: 200 }} />
</Grid>
<Grid item sx={{ textAlign: "center" }} />
<Grid item>
{!session ? (
<Grid
container
spacing={3}
direction="column"
alignItems="center"
sx={{ width: 450, mt: 1 }}
<Grid
item
container
direction="row"
justifyContent="center"
alignItems="center"
>
<Grid item>
<Box
sx={{
width: "70px",
height: "70px",
margin: "0 auto",
}}
>
<Grid item sx={{ width: "100%" }}>
<IconButton
sx={buttonStyles}
onClick={() =>
signIn("google", {
callbackUrl: `${origin}/setup`,
})
}
>
<GoogleIcon sx={{ mr: 1 }} />
Google
</IconButton>
<Image
src={LinkLogo}
alt="Link logo"
width={70}
height={70}
style={{
objectFit: "cover",
filter: "grayscale(100) brightness(100)",
}}
/>
</Box>
</Grid>
<Grid item>
<Typography
variant="h2"
sx={{
fontSize: 36,
color: "white",
fontWeight: 700,
mt: 1,
ml: 0.5,
fontFamily: "Poppins",
}}
>
CDR Link
</Typography>
</Grid>
</Grid>
<Grid item sx={{ width: "100%" }}>
{!session ? (
<Container
maxWidth="xs"
sx={{
p: 3,
mt: 3,
}}
>
<Grid
container
spacing={3}
direction="column"
alignItems="center"
>
{error ? (
<Grid item sx={{ width: "100%" }}>
<Box sx={{ backgroundColor: "red", p: 3 }}>
<Typography
variant="body1"
sx={{
fontSize: 18,
color: "white",
textAlign: "center",
}}
>
{`${error} error`}
</Typography>
</Box>
</Grid>
) : null}
<Grid item sx={{ width: "100%" }}>
<IconButton
sx={buttonStyles}
onClick={() =>
signIn("google", {
callbackUrl: `${origin}/setup`,
})
}
>
<GoogleIcon sx={{ mr: 1 }} />
Sign in with Google
</IconButton>
</Grid>
<Grid item sx={{ width: "100%" }}>
<IconButton
aria-label="Sign in with Apple"
sx={buttonStyles}
onClick={() =>
signIn("apple", {
callbackUrl: `${window.location.origin}/setup`,
})
}
>
<AppleIcon sx={{ mr: 1 }} />
Sign in with Apple
</IconButton>
</Grid>
<Grid>
<Typography
variant="body1"
sx={{
fontSize: 18,
color: white,
textAlign: "center",
mt: 3,
}}
>
or
</Typography>
</Grid>
<Grid item sx={{ width: "100%" }}>
<TextField
value={email}
onChange={(e) => setEmail(e.target.value)}
label="Email"
variant="filled"
size="small"
fullWidth
sx={{ ...fieldStyles, backgroundColor: white }}
/>
</Grid>
<Grid item sx={{ ...fieldStyles, width: "100%" }}>
<TextField
value={password}
onChange={(e) => setPassword(e.target.value)}
label="Password"
variant="filled"
size="small"
fullWidth
sx={{ backgroundColor: white }}
type="password"
/>
</Grid>
<Grid item sx={{ width: "100%" }}>
<IconButton
sx={buttonStyles}
onClick={() =>
signIn("credentials", {
email,
password,
callbackUrl: `${origin}/setup`,
})
}
>
<KeyIcon sx={{ mr: 1 }} />
Sign in with Zammad credentials
</IconButton>
</Grid>
</Grid>
{/*
<Grid item sx={{ width: "100%" }}>
<IconButton
sx={buttonStyles}
onClick={() =>
signIn("apple", {
callbackUrl: `${window.location.origin}/setup`,
})
}
>
<AppleIcon sx={{ mr: 1 }} />
</IconButton>
</Grid>*/}
<Grid item sx={{ mt: 2 }} />
</Grid>
</Container>
) : null}
{session ? (
<Box component="h4">
@ -79,6 +230,6 @@ export const Login: FC<LoginProps> = ({ session }) => {
</Grid>
</Grid>
</Container>
</>
</Box>
);
};

View file

@ -0,0 +1,14 @@
"use client";
import dynamic from "next/dynamic";
type ClientOnlyProps = { children: JSX.Element };
const ClientOnly = (props: ClientOnlyProps) => {
const { children } = props;
return children;
};
export default dynamic(() => Promise.resolve(ClientOnly), {
ssr: false,
});

View file

@ -17,12 +17,14 @@ import {
import {
FeaturedPlayList as FeaturedPlayListIcon,
Person as PersonIcon,
Analytics as AnalyticsIcon,
Insights as InsightsIcon,
Logout as LogoutIcon,
Cottage as CottageIcon,
Settings as SettingsIcon,
ExpandCircleDown as ExpandCircleDownIcon,
Dvr as DvrIcon,
Assessment as AssessmentIcon,
LibraryBooks as LibraryBooksIcon,
} from "@mui/icons-material";
import { usePathname } from "next/navigation";
import Link from "next/link";
@ -161,7 +163,10 @@ interface SidebarProps {
export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
const pathname = usePathname();
const { data: session } = useSession();
console.log({ session });
const username = session?.user?.name || "User";
// @ts-ignore
const roles = session?.user?.roles || [];
const { data: overviewData, error: overviewError }: any = useSWR(
{
document: getTicketOverviewCountsQuery,
@ -419,17 +424,25 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
<MenuItem
name="Knowledge Base"
href="/knowledge"
Icon={CottageIcon}
Icon={LibraryBooksIcon}
iconSize={20}
selected={pathname.endsWith("/knowledge")}
open={open}
/>
<MenuItem
name="Reporting"
href="/reporting"
Icon={AssessmentIcon}
iconSize={20}
selected={pathname.endsWith("/reporting")}
open={open}
/>
<MenuItem
name="Leafcutter"
href="/leafcutter"
Icon={AnalyticsIcon}
Icon={InsightsIcon}
iconSize={20}
selected={pathname.endsWith("/leafcutter")}
selected={false}
open={open}
/>
<Collapse
@ -471,7 +484,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
<MenuItem
name="About"
href="/leafcutter/about"
Icon={AnalyticsIcon}
Icon={InsightsIcon}
iconSize={0}
selected={pathname.endsWith("/leafcutter/about")}
open={open}
@ -486,49 +499,57 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
selected={pathname.endsWith("/profile")}
open={open}
/>
<MenuItem
name="Admin"
href="/admin/zammad"
Icon={SettingsIcon}
iconSize={20}
open={open}
/>
<Collapse
in={pathname.startsWith("/admin/")}
timeout="auto"
unmountOnExit
onClick={undefined}
>
<List component="div" disablePadding>
{roles.includes("admin") && (
<>
<MenuItem
name="Zammad Settings"
name="Admin"
href="/admin/zammad"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/admin/zammad")}
Icon={SettingsIcon}
iconSize={20}
open={open}
/>
<MenuItem
name="Metamigo"
href="/admin/metamigo"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/admin/metamigo")}
open={open}
/>
<MenuItem
name="Label Studio"
href="/admin/label-studio"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/admin/label-studio")}
open={open}
/>
</List>
</Collapse>
<Collapse
in={pathname.startsWith("/admin/")}
timeout="auto"
unmountOnExit
onClick={undefined}
>
<List component="div" disablePadding>
<MenuItem
name="Zammad Settings"
href="/admin/zammad"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/admin/zammad")}
open={open}
/>
{false && roles.includes("metamigo") && (
<MenuItem
name="Metamigo"
href="/admin/metamigo"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/admin/metamigo")}
open={open}
/>
)}
{roles.includes("label_studio") && (
<MenuItem
name="Label Studio"
href="/admin/label-studio"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/admin/label-studio")}
open={open}
/>
)}
</List>
</Collapse>
</>
)}
<MenuItem
name="Zammad Interface"
href="/proxy/zammad"
href="/zammad"
Icon={DvrIcon}
iconSize={20}
open={open}

View file

@ -15,8 +15,7 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
}) => {
const router = useRouter();
const [display, setDisplay] = useState("none");
const url = `/proxy/zammad${path}`;
console.log({ url });
const url = `/zammad${path}`;
return (
// @ts-ignore
@ -33,8 +32,7 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
linkElement.contentDocument &&
linkElement.contentDocument?.querySelector &&
linkElement.contentDocument.querySelector("#navigation") &&
linkElement.contentDocument.querySelector("body") &&
linkElement.contentDocument.querySelector(".sidebar")
linkElement.contentDocument.querySelector("body")
) {
// @ts-ignore
linkElement.contentDocument.querySelector("#navigation").style =
@ -43,7 +41,10 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
linkElement.contentDocument.querySelector("body").style =
"font-family: Arial";
if (hideSidebar) {
if (
hideSidebar &&
linkElement.contentDocument.querySelector(".sidebar")
) {
// @ts-ignore
linkElement.contentDocument.querySelector(".sidebar").style =
"display: none";

View file

@ -0,0 +1,16 @@
"use client";
import { FC } from "react";
import { ApolloProvider } from "@apollo/client";
import { apolloClient } from "../_lib/apollo-client";
import dynamic from "next/dynamic";
const MetamigoAdmin = dynamic(() => import("./MetamigoAdmin"), {
ssr: false,
});
export const Admin: FC = () => (
<ApolloProvider client={apolloClient}>
<MetamigoAdmin />
</ApolloProvider>
);

View file

@ -6,13 +6,11 @@ import { FC, useEffect, useState } from "react";
import { Admin, Resource } from "react-admin";
import { useApolloClient } from "@apollo/client";
import polyglotI18nProvider from "ra-i18n-polyglot";
import { ThemeProvider, createTheme } from "@mui/material";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import { metamigoDataProvider } from "../_lib/dataprovider";
import { theme } from "./layout/themes";
import { Layout } from "./layout";
import englishMessages from "../_i18n/en";
import users from "./users";
import accounts from "./accounts";
import whatsappBots from "./whatsapp/bots";
import whatsappMessages from "./whatsapp/messages";
import whatsappAttachments from "./whatsapp/attachments";
@ -42,29 +40,25 @@ const MetamigoAdmin: FC = () => {
}, [client]);
return (
dataProvider && (
<ThemeProvider theme={muiTheme}>
<Admin
disableTelemetry
dataProvider={dataProvider}
layout={Layout}
i18nProvider={i18nProvider}
// @ts-ignore
loginPage={AdminLogin}
// @ts-ignore
authProvider={authProvider}
>
<Resource name="webhooks" {...webhooks} />
<Resource name="whatsappBots" {...whatsappBots} />
<Resource name="whatsappMessages" {...whatsappMessages} />
<Resource name="whatsappAttachments" {...whatsappAttachments} />
<Resource name="signalBots" {...signalBots} />
<Resource name="voiceProviders" {...voiceProviders} />
<Resource name="voiceLines" {...voiceLines} />
<Resource name="users" {...users} />
<Resource name="accounts" {...accounts} />
<Resource name="languages" />
</Admin>
</ThemeProvider>
<Admin
disableTelemetry
dataProvider={dataProvider}
layout={Layout}
i18nProvider={i18nProvider}
// @ts-ignore
loginPage={AdminLogin}
// @ts-ignore
authProvider={authProvider}
>
<Resource name="webhooks" {...webhooks} />
<Resource name="whatsappBots" {...whatsappBots} />
<Resource name="whatsappMessages" {...whatsappMessages} />
<Resource name="whatsappAttachments" {...whatsappAttachments} />
<Resource name="signalBots" {...signalBots} />
<Resource name="voiceProviders" {...voiceProviders} />
<Resource name="voiceLines" {...voiceLines} />
<Resource name="languages" />
</Admin>
)
);
};

View file

@ -1,30 +0,0 @@
"use client";
import { FC } from "react";
import { Grid } from "@mui/material";
import Iframe from "react-iframe";
type MetamigoWrapperProps = {
path: string;
};
export const MetamigoWrapper: FC<MetamigoWrapperProps> = ({ path }) => {
return (
<Grid
container
spacing={0}
sx={{ height: "100%", width: "100%" }}
direction="column"
>
<Grid item sx={{ height: "100vh", width: "100%" }}>
<Iframe
id="metamigo"
url="/proxy/metamigo"
width="100%"
height="100%"
frameBorder={0}
/>
</Grid>
</Grid>
);
};

View file

@ -1,7 +1,7 @@
"use client";
import { FC } from "react";
import { makeStyles } from "@mui/styles";
// import { makeStyles } from "@mui/styles";
import {
SimpleForm,
TextInput,
@ -15,6 +15,7 @@ import {
} from "react-admin";
import { useSession } from "next-auth/react";
/*
const useStyles = makeStyles((_theme: any) => ({
defaultToolbar: {
flex: 1,
@ -22,14 +23,14 @@ const useStyles = makeStyles((_theme: any) => ({
justifyContent: "space-between",
},
}));
*/
type AccountEditToolbarProps = {
record?: any;
};
const AccountEditToolbar: FC<AccountEditToolbarProps> = (props) => {
const { data: session } = useSession();
const classes = useStyles(props);
const classes: any = {}; // useStyles(props);
return (
<Toolbar className={classes.defaultToolbar} {...props}>
<DeleteButton disabled={session?.user?.email === props.record?.userId} />

View file

@ -4,8 +4,9 @@ import { forwardRef } from "react";
import { AppBar, UserMenu, MenuItemLink, useTranslate } from "react-admin";
import Typography from "@mui/material/Typography";
import SettingsIcon from "@mui/icons-material/Settings";
import { makeStyles } from "@mui/styles";
import { colors } from "app/_styles/theme";
// import { makeStyles } from "@mui/styles";
/*
const useStyles = makeStyles({
title: {
flex: 1,
@ -17,7 +18,7 @@ const useStyles = makeStyles({
flex: 1,
},
});
*/
// eslint-disable-next-line react/display-name
const ConfigurationMenu = forwardRef<any, any>((props, ref) => {
const translate = useTranslate();
@ -33,21 +34,16 @@ const ConfigurationMenu = forwardRef<any, any>((props, ref) => {
);
});
const CustomUserMenu = (props: any) => (
<UserMenu {...props}>
<ConfigurationMenu />
</UserMenu>
);
const CustomAppBar = (props: any) => {
const classes = useStyles();
const classes: any = {}; // useStyles();
const { darkLavender } = colors;
return (
<AppBar
{...props}
elevation={1}
userMenu={<CustomUserMenu />}
elevation={0}
userMenu={<div />}
position="sticky"
sx={{ mt: -1 }}
sx={{ mt: -1, backgroundColor: darkLavender }}
>
<Typography
variant="h6"

View file

@ -2,15 +2,11 @@
/* eslint-disable camelcase */
import { FC, useState } from "react";
// import { useSelector } from "react-redux";
import SecurityIcon from "@mui/icons-material/Security";
import VoiceIcon from "@mui/icons-material/PhoneInTalk";
import { Box } from "@mui/material";
import { useTheme } from "@mui/styles";
// import { useTheme } from "@mui/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTranslate, MenuItemLink } from "react-admin";
import users from "../users";
import accounts from "../accounts";
import webhooks from "../webhooks";
import voiceLines from "../voice/voicelines";
import voiceProviders from "../voice/providers";
@ -26,11 +22,10 @@ export const Menu: FC = ({ onMenuClick, logout, dense = false }: any) => {
menuSecurity: false,
});
const translate = useTranslate();
const theme = useTheme();
const theme: any = {}; // useTheme();
// @ts-ignore
const isXSmall = useMediaQuery(theme.breakpoints.down("xs"));
const isXSmall = false; // useMediaQuery(theme?.breakpoints?.down("xs"));
const open = true; // useSelector((state: any) => state.admin.ui.sidebarOpen);
// useSelector((state: any) => state.theme); // force rerender on theme change
const handleToggle = (menu: MenuName) => {
setState((state: any) => ({ ...state, [menu]: !state[menu] }));
@ -97,35 +92,6 @@ export const Menu: FC = ({ onMenuClick, logout, dense = false }: any) => {
sidebarIsOpen={open}
dense={dense}
/>
<SubMenu
handleToggle={() => handleToggle("menuSecurity")}
isOpen={state.menuSecurity}
sidebarIsOpen={open}
name="pos.menu.security"
icon={<SecurityIcon />}
dense={dense}
>
<MenuItemLink
to={`/users`}
primaryText={translate(`resources.users.name`, {
smart_count: 2,
})}
leftIcon={<users.icon />}
onClick={onMenuClick}
sidebarIsOpen={open}
dense={dense}
/>
<MenuItemLink
to={`/accounts`}
primaryText={translate(`resources.accounts.name`, {
smart_count: 2,
})}
leftIcon={<accounts.icon />}
onClick={onMenuClick}
sidebarIsOpen={open}
dense={dense}
/>
</SubMenu>
{isXSmall && logout}
</Box>
);

View file

@ -8,9 +8,10 @@ import ListItemIcon from "@mui/material/ListItemIcon";
import Typography from "@mui/material/Typography";
import Collapse from "@mui/material/Collapse";
import Tooltip from "@mui/material/Tooltip";
import { makeStyles } from "@mui/styles";
// import { makeStyles } from "@mui/styles";
import { useTranslate } from "react-admin";
/*
const useStyles = makeStyles((theme: any) => ({
icon: { minWidth: theme.spacing(5) },
sidebarIsOpen: {
@ -26,7 +27,7 @@ const useStyles = makeStyles((theme: any) => ({
},
},
}));
*/
type SubMenuProps = PropsWithChildren<{
dense: boolean;
handleToggle: () => void;
@ -46,7 +47,7 @@ export const SubMenu: FC<SubMenuProps> = ({
dense,
}: any) => {
const translate = useTranslate();
const classes = useStyles();
const classes: any = {}; // = useStyles();
const header = (
// @ts-ignore

View file

@ -45,8 +45,11 @@ export const theme = {
},
},
typography: {
h6: { fontSize: 16, fontWeight: 600, color: "#1bb1bb" },
fontFamily: "Poppins, sans-serif",
fontWeight: 900,
h6: { fontSize: 16, fontWeight: 600, color: "red" },
},
/*
overrides: {
RaMenuItemLink: {
root: {
@ -96,4 +99,5 @@ export const theme = {
},
},
},
*/
};

View file

@ -231,7 +231,7 @@ const VerificationCaptcha = ({
.replace(/signalcaptcha:\/\//, "")
.replace("“", "")
.replace("”", "")
.trim()
.trim(),
);
else setCode(value);
};
@ -294,7 +294,7 @@ const VerificationCodeInput = ({
headers: {
"Content-Type": "application/json",
},
}
},
);
setSubmitting(false);
const responseBody = await response.json();
@ -302,12 +302,10 @@ const VerificationCodeInput = ({
if (response.status === 200) {
confirmVerification();
} else if (responseBody.message)
// @ts-expect-error
setSubmissionError(`Error: ${responseBody.message}`);
else {
setSubmissionError(
// @ts-expect-error
"There was an error, sorry about that. Please try again later or contact support."
"There was an error, sorry about that. Please try again later or contact support.",
);
}
};

View file

@ -1,6 +1,6 @@
"use client";
import { makeStyles } from "@mui/styles";
// import { makeStyles } from "@mui/styles";
import {
SimpleForm,
TextInput,
@ -16,6 +16,7 @@ import {
import { useSession } from "next-auth/react";
import { UserRoleInput } from "./shared";
/*
const useStyles = makeStyles((_theme: any) => ({
defaultToolbar: {
flex: 1,
@ -23,14 +24,16 @@ const useStyles = makeStyles((_theme: any) => ({
justifyContent: "space-between",
},
}));
*/
const UserEditToolbar = (props: any) => {
const classes = useStyles();
const classes: any = {}; // = useStyles();
const redirect = useRedirect();
const record = useRecordContext();
const {session} = props;
const record = useRecordContext();
const { session } = props;
const shouldDisableDelete = !session || !session.user || session.user.id === record.id;
const shouldDisableDelete =
!session || !session.user || session.user.id === record.id;
return (
<Toolbar className={classes.defaultToolbar}>

View file

@ -6,7 +6,7 @@ import dynamic from "next/dynamic";
import MicIcon from "@mui/icons-material/Mic";
import StopIcon from "@mui/icons-material/Stop";
import Button from "@mui/material/Button";
import { useTheme } from "@mui/styles"; // makeStyles,
// import { useTheme } from "@mui/styles"; // makeStyles,
// import AudioPlayer from "material-ui-audio-player";
import { useStopwatch } from "react-timer-hook";
import style from "./MicInput.module.css";
@ -15,10 +15,10 @@ import style from "./MicInput.module.css";
const ReactMic = dynamic<any>(
() => {
throw new Error(
"MIC INPUT FEATURE IS DISABLED"
"MIC INPUT FEATURE IS DISABLED",
); /* return import("react-mic").then((mod) => mod.ReactMic); */
},
{ ssr: false }
{ ssr: false },
);
const blobToDataUri = (blob: Blob) => {
@ -54,7 +54,7 @@ const blobToResult = async (blob: Blob) => {
const MicInput = (props: any) => {
const { seconds, minutes, hours, start, reset, pause } = useStopwatch();
const theme = useTheme();
const theme: any = {}; // useTheme();
const {
field: { onChange }, // value
} = useInput(props);

Some files were not shown because too many files have changed in this diff Show more