Develop
This commit is contained in:
parent
7ca5f2d45a
commit
f901f203b0
302 changed files with 9897 additions and 10332 deletions
114
apps/leafcutter/app/(login)/login/_components/Login.tsx
Normal file
114
apps/leafcutter/app/(login)/login/_components/Login.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/legacy/image";
|
||||
import { Box, Grid, Container, IconButton } from "@mui/material";
|
||||
import { Apple as AppleIcon, Google as GoogleIcon } from "@mui/icons-material";
|
||||
import { useTranslate } from "react-polyglot";
|
||||
import { LanguageSelect } from "app/_components/LanguageSelect";
|
||||
import LeafcutterLogoLarge from "images/leafcutter-logo-large.png";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useAppContext } from "app/_components/AppProvider";
|
||||
|
||||
type LoginProps = {
|
||||
session: any;
|
||||
};
|
||||
|
||||
export const Login: FC<LoginProps> = ({ session }) => {
|
||||
const t = useTranslate();
|
||||
const {
|
||||
colors: { leafcutterElectricBlue, lightGray },
|
||||
typography: { h1, h4 },
|
||||
} = useAppContext();
|
||||
const buttonStyles = {
|
||||
backgroundColor: lightGray,
|
||||
borderRadius: 500,
|
||||
width: "100%",
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container direction="row-reverse" sx={{ p: 3 }}>
|
||||
<Grid item>
|
||||
<LanguageSelect />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Container maxWidth="md" sx={{ mt: 3, mb: 20 }}>
|
||||
<Grid container spacing={2} direction="column" alignItems="center">
|
||||
<Grid item>
|
||||
<Box sx={{ maxWidth: 200 }}>
|
||||
<Image src={LeafcutterLogoLarge} alt="" objectFit="fill" />
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item sx={{ textAlign: "center" }}>
|
||||
<Box component="h1" sx={{ ...h1, color: leafcutterElectricBlue }}>
|
||||
{t("welcomeToLeafcutter")}
|
||||
</Box>
|
||||
<Box component="h4" sx={{ ...h4, mt: 1 }}>
|
||||
{t("welcomeToLeafcutterDescription")}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
{!session ? (
|
||||
<Grid
|
||||
container
|
||||
spacing={3}
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
sx={{ width: 450, mt: 1 }}
|
||||
>
|
||||
<Grid item sx={{ width: "100%" }}>
|
||||
<IconButton
|
||||
sx={buttonStyles}
|
||||
onClick={() =>
|
||||
signIn("google", {
|
||||
callbackUrl: `${window.location.origin}/setup`,
|
||||
})
|
||||
}
|
||||
>
|
||||
<GoogleIcon sx={{ mr: 1 }} />
|
||||
{`${t("signInWith")} Google`}
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item sx={{ width: "100%" }}>
|
||||
<IconButton
|
||||
sx={buttonStyles}
|
||||
onClick={() =>
|
||||
signIn("apple", {
|
||||
callbackUrl: `${window.location.origin}/setup`,
|
||||
})
|
||||
}
|
||||
>
|
||||
<AppleIcon sx={{ mr: 1 }} />
|
||||
{`${t("signInWith")} Apple`}
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item sx={{ mt: 2 }}>
|
||||
<Box>
|
||||
{t("dontHaveAccount")}{" "}
|
||||
<Link href="mailto:info@digiresilience.org">
|
||||
{t("requestAccessHere")}
|
||||
</Link>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : null}
|
||||
{session ? (
|
||||
<>
|
||||
<Box component="h4" sx={h4}>
|
||||
{`${t("welcome")}, ${
|
||||
session.user.name ?? session.user.email
|
||||
}.`}
|
||||
</Box>
|
||||
<Link href="/">{t("goHome")}</Link>
|
||||
</>
|
||||
) : null}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
16
apps/leafcutter/app/(login)/login/page.tsx
Normal file
16
apps/leafcutter/app/(login)/login/page.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Metadata } from "next";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "app/_lib/auth";
|
||||
import { Login } from "./_components/Login";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Login",
|
||||
};
|
||||
|
||||
export default async function Page() {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
return <Login session={session} />;
|
||||
}
|
||||
|
||||
|
||||
163
apps/leafcutter/app/(main)/about/_components/About.tsx
Normal file
163
apps/leafcutter/app/(main)/about/_components/About.tsx
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
"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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
35
apps/leafcutter/app/(main)/about/_components/AboutBox.tsx
Normal file
35
apps/leafcutter/app/(main)/about/_components/AboutBox.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
5
apps/leafcutter/app/(main)/about/page.tsx
Normal file
5
apps/leafcutter/app/(main)/about/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { About } from './_components/About';
|
||||
|
||||
export default function Page() {
|
||||
return <About />;
|
||||
}
|
||||
64
apps/leafcutter/app/(main)/create/_components/Create.tsx
Normal file
64
apps/leafcutter/app/(main)/create/_components/Create.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
"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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
8
apps/leafcutter/app/(main)/create/page.tsx
Normal file
8
apps/leafcutter/app/(main)/create/page.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { getTemplates } from "app/_lib/opensearch";
|
||||
import { Create } from "./_components/Create";
|
||||
|
||||
export default async function Page() {
|
||||
const templates = await getTemplates(100);
|
||||
|
||||
return <Create templates={templates} />;
|
||||
}
|
||||
102
apps/leafcutter/app/(main)/faq/_components/FAQ.tsx
Normal file
102
apps/leafcutter/app/(main)/faq/_components/FAQ.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
"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} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
5
apps/leafcutter/app/(main)/faq/page.tsx
Normal file
5
apps/leafcutter/app/(main)/faq/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { FAQ } from "./_components/FAQ";
|
||||
|
||||
export default function Page() {
|
||||
return <FAQ />;
|
||||
}
|
||||
22
apps/leafcutter/app/(main)/layout.tsx
Normal file
22
apps/leafcutter/app/(main)/layout.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { ReactNode } from "react";
|
||||
import "app/_styles/global.css";
|
||||
import "@fontsource/poppins/400.css";
|
||||
import "@fontsource/poppins/700.css";
|
||||
import "@fontsource/roboto/400.css";
|
||||
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'
|
||||
|
||||
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>;
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
"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} />
|
||||
);
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/* eslint-disable no-underscore-dangle */
|
||||
// import { Client } from "@opensearch-project/opensearch";
|
||||
import { Preview } from "./_components/Preview";
|
||||
// import { createVisualization } from "lib/opensearch";
|
||||
|
||||
export default function Page() {
|
||||
return <Preview visualization={undefined} visualizationType={""} data={[]}/>;
|
||||
}
|
||||
|
||||
/*
|
||||
export const getServerSideProps: GetServerSideProps = async (
|
||||
context: GetServerSidePropsContext
|
||||
) => {
|
||||
|
||||
|
||||
const {
|
||||
visualizationID,
|
||||
searchQuery,
|
||||
visualizationType = "table",
|
||||
} = context.query;
|
||||
|
||||
const node = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
|
||||
const client = new Client({
|
||||
node,
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
res.props.visualizationType = visualizationType as string;
|
||||
|
||||
if (visualizationType !== "rawData") {
|
||||
await createVisualization({
|
||||
id: visualizationID as string,
|
||||
query: await JSON.parse(decodeURI(searchQuery as string)),
|
||||
kind: visualizationType as string,
|
||||
});
|
||||
const rawResponse = await client.search({
|
||||
index: ".kibana_1",
|
||||
size: 200,
|
||||
});
|
||||
const response = rawResponse.body;
|
||||
|
||||
const hits = response.hits.hits.filter(
|
||||
(hit) => hit._id.split(":")[1] === visualizationID[0]
|
||||
);
|
||||
const hit = hits[0];
|
||||
res.props.visualization = {
|
||||
id: hit._id.split(":")[1],
|
||||
title: hit._source.visualization.title,
|
||||
description: hit._source.visualization.description,
|
||||
url: `/app/visualize?security_tenant=global#/edit/${
|
||||
hit._id.split(":")[1]
|
||||
}?embed=true`,
|
||||
};
|
||||
}
|
||||
|
||||
const rawQuery = await JSON.parse(decodeURI(searchQuery as string));
|
||||
const query = {
|
||||
bool: {
|
||||
should: [],
|
||||
must_not: [],
|
||||
},
|
||||
};
|
||||
|
||||
if (rawQuery.impactedTechnology.values.length > 0) {
|
||||
rawQuery.impactedTechnology.values.forEach((value) => {
|
||||
query.bool.should.push({
|
||||
match: { technology: value },
|
||||
});
|
||||
});
|
||||
}
|
||||
console.log({ query });
|
||||
const dataResponse = await client.search({
|
||||
index: "demo_data",
|
||||
size: 200,
|
||||
body: { query },
|
||||
});
|
||||
console.log({ dataResponse });
|
||||
res.props.data = dataResponse.body.hits.hits.map((hit) => ({
|
||||
id: hit._id,
|
||||
...hit._source,
|
||||
}));
|
||||
console.log({ data: res.props.data });
|
||||
console.log(res.props.data[0]);
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
*/
|
||||
49
apps/leafcutter/app/(main)/setup/_components/Setup.tsx
Normal file
49
apps/leafcutter/app/(main)/setup/_components/Setup.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
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";
|
||||
|
||||
export const Setup: FC = () => {
|
||||
const {
|
||||
colors: { leafcutterElectricBlue },
|
||||
} = useAppContext();
|
||||
const router = useRouter();
|
||||
useLayoutEffect(() => {
|
||||
setTimeout(() => router.push("/"), 4000);
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
sx={{ width: "100%", height: 700 }}
|
||||
direction="row"
|
||||
justifyContent="space-around"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
sx={{
|
||||
width: "200px",
|
||||
height: 700,
|
||||
textAlign: "center",
|
||||
margin: "0 auto",
|
||||
pt: 30,
|
||||
}}
|
||||
>
|
||||
<Iframe url="/app/home" height="1" width="1" frameBorder={0} />
|
||||
<CircularProgress
|
||||
size={80}
|
||||
thickness={5}
|
||||
sx={{ color: leafcutterElectricBlue }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default Setup;
|
||||
6
apps/leafcutter/app/(main)/setup/page.tsx
Normal file
6
apps/leafcutter/app/(main)/setup/page.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { Setup } from './_components/Setup';
|
||||
|
||||
export default function Page() {
|
||||
return <Setup />;
|
||||
}
|
||||
|
||||
72
apps/leafcutter/app/(main)/trends/_components/Trends.tsx
Normal file
72
apps/leafcutter/app/(main)/trends/_components/Trends.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
8
apps/leafcutter/app/(main)/trends/page.tsx
Normal file
8
apps/leafcutter/app/(main)/trends/page.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { getTrends } from "app/_lib/opensearch";
|
||||
import { Trends } from "./_components/Trends";
|
||||
|
||||
export default async function Page() {
|
||||
const visualizations = await getTrends(25);
|
||||
|
||||
return <Trends visualizations={visualizations} />;
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/* eslint-disable no-underscore-dangle */
|
||||
import { Client } from "@opensearch-project/opensearch";
|
||||
import { VisualizationDetail } from "app/_components/VisualizationDetail";
|
||||
|
||||
const getVisualization = async (visualizationID: string) => {
|
||||
const node = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
|
||||
const client = new Client({
|
||||
node,
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
|
||||
const rawResponse = await client.search({
|
||||
index: ".kibana_1",
|
||||
size: 200,
|
||||
});
|
||||
const response = rawResponse.body;
|
||||
|
||||
const hits = response.hits.hits.filter(
|
||||
(hit: any) => hit._id.split(":")[1] === visualizationID[0]
|
||||
);
|
||||
const hit = hits[0];
|
||||
const visualization = {
|
||||
id: hit._id.split(":")[1],
|
||||
title: hit._source.visualization.title,
|
||||
description: hit._source.visualization.description,
|
||||
url: `/app/visualize?security_tenant=global#/edit/${
|
||||
hit._id.split(":")[1]
|
||||
}?embed=true`,
|
||||
};
|
||||
|
||||
return visualization;
|
||||
};
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
visualizationID: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default async function Page({ params: { visualizationID } }: PageProps) {
|
||||
const visualization = await getVisualization(visualizationID);
|
||||
|
||||
return <VisualizationDetail {...visualization} editing={false} />;
|
||||
}
|
||||
55
apps/leafcutter/app/_components/AccountButton.tsx
Normal file
55
apps/leafcutter/app/_components/AccountButton.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import { signOut } from "next-auth/react";
|
||||
import { Button, Box, Menu, MenuItem } from "@mui/material";
|
||||
import { useTranslate } from "react-polyglot";
|
||||
import UserIcon from "images/user-icon.png";
|
||||
import {
|
||||
usePopupState,
|
||||
bindTrigger,
|
||||
bindMenu,
|
||||
} from "material-ui-popup-state/hooks";
|
||||
import { useAppContext } from "./AppProvider";
|
||||
|
||||
export const AccountButton: FC = () => {
|
||||
const t = useTranslate();
|
||||
const {
|
||||
colors: { leafcutterElectricBlue },
|
||||
} = useAppContext();
|
||||
const popupState = usePopupState({ variant: "popover", popupId: "account" });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
{...bindTrigger(popupState)}
|
||||
color="secondary"
|
||||
sx={{
|
||||
backgroundColor: leafcutterElectricBlue,
|
||||
width: "40px",
|
||||
height: "40px",
|
||||
minWidth: "40px",
|
||||
p: 0,
|
||||
borderRadius: "500px",
|
||||
":hover": {
|
||||
backgroundColor: leafcutterElectricBlue,
|
||||
opacity: 0.8,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Image src={UserIcon} alt="account" width={30} height={30} />
|
||||
</Button>
|
||||
<Menu {...bindMenu(popupState)}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
popupState.close();
|
||||
signOut({ callbackUrl: "/" });
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: "100%" }}>{t("signOut")}</Box>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
161
apps/leafcutter/app/_components/AppProvider.tsx
Normal file
161
apps/leafcutter/app/_components/AppProvider.tsx
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
FC,
|
||||
createContext,
|
||||
useContext,
|
||||
useReducer,
|
||||
useState,
|
||||
PropsWithChildren,
|
||||
} from "react";
|
||||
import { colors, typography } from "app/_styles/theme";
|
||||
|
||||
const basePath = process.env.GITLAB_CI
|
||||
? "/link/link-stack/apps/leafcutter"
|
||||
: "";
|
||||
const imageURL = (image: any) =>
|
||||
typeof image === "string" ? `${basePath}${image}` : `${basePath}${image.src}`;
|
||||
|
||||
const AppContext = createContext({
|
||||
colors,
|
||||
typography,
|
||||
imageURL,
|
||||
query: null as any,
|
||||
updateQuery: null as any,
|
||||
updateQueryType: null as any,
|
||||
replaceQuery: null as any,
|
||||
clearQuery: null as any,
|
||||
foundCount: 0,
|
||||
setFoundCount: null as any,
|
||||
});
|
||||
|
||||
export const AppProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const initialState = {
|
||||
incidentType: {
|
||||
display: "Incident Type",
|
||||
queryType: "include",
|
||||
values: [],
|
||||
},
|
||||
relativeDate: {
|
||||
display: "Relative Date",
|
||||
queryType: null,
|
||||
values: [],
|
||||
},
|
||||
startDate: {
|
||||
display: "Start Date",
|
||||
queryType: null,
|
||||
values: [],
|
||||
},
|
||||
endDate: {
|
||||
display: "End Date",
|
||||
queryType: null,
|
||||
values: [],
|
||||
},
|
||||
targetedGroup: {
|
||||
display: "Targeted Group",
|
||||
queryType: "include",
|
||||
values: [],
|
||||
},
|
||||
platform: {
|
||||
display: "Platform",
|
||||
queryType: "include",
|
||||
values: [],
|
||||
},
|
||||
device: {
|
||||
display: "Device",
|
||||
queryType: "include",
|
||||
values: [],
|
||||
},
|
||||
service: {
|
||||
display: "Service",
|
||||
queryType: "include",
|
||||
values: [],
|
||||
},
|
||||
maker: {
|
||||
display: "Maker",
|
||||
queryType: "include",
|
||||
values: [],
|
||||
},
|
||||
country: {
|
||||
display: "Country",
|
||||
queryType: "include",
|
||||
values: [],
|
||||
},
|
||||
subregion: {
|
||||
display: "Subregion",
|
||||
queryType: "include",
|
||||
values: [],
|
||||
},
|
||||
continent: {
|
||||
display: "Continent",
|
||||
queryType: "include",
|
||||
values: [],
|
||||
},
|
||||
};
|
||||
const reducer = (state: any, action: any) => {
|
||||
const key = action.payload?.[0];
|
||||
if (!key) {
|
||||
throw new Error("Unknown key");
|
||||
}
|
||||
const newState = { ...state };
|
||||
switch (action.type) {
|
||||
case "UPDATE":
|
||||
newState[key].values = action.payload[key].values;
|
||||
return newState;
|
||||
case "UPDATE_TYPE":
|
||||
newState[key].queryType = action.payload[key].queryType;
|
||||
return newState;
|
||||
case "REPLACE":
|
||||
return Object.keys(action.payload).reduce((acc: any, cur: string) => {
|
||||
if (["startDate", "endDate"].includes(cur)) {
|
||||
const rawDate = action.payload[cur].values[0];
|
||||
const date = new Date(rawDate);
|
||||
acc[cur] = {
|
||||
...action.payload[cur],
|
||||
values: rawDate && date ? [date] : [],
|
||||
};
|
||||
} else {
|
||||
acc[cur] = action.payload[cur];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
case "CLEAR":
|
||||
return initialState;
|
||||
default:
|
||||
throw new Error("Unknown action type");
|
||||
}
|
||||
};
|
||||
|
||||
const [query, dispatch] = useReducer(reducer, initialState);
|
||||
const updateQuery = (payload: any) => dispatch({ type: "UPDATE", payload });
|
||||
const updateQueryType = (payload: any) =>
|
||||
dispatch({ type: "UPDATE_TYPE", payload });
|
||||
const replaceQuery = (payload: any) => dispatch({ type: "REPLACE", payload });
|
||||
const clearQuery = () => dispatch({ type: "CLEAR" });
|
||||
const [foundCount, setFoundCount] = useState(0);
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
||||
value={{
|
||||
colors,
|
||||
typography,
|
||||
imageURL,
|
||||
query,
|
||||
updateQuery,
|
||||
updateQueryType,
|
||||
replaceQuery,
|
||||
clearQuery,
|
||||
foundCount,
|
||||
setFoundCount,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useAppContext() {
|
||||
return useContext(AppContext);
|
||||
}
|
||||
42
apps/leafcutter/app/_components/Button.tsx
Normal file
42
apps/leafcutter/app/_components/Button.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
115
apps/leafcutter/app/_components/Footer.tsx
Normal file
115
apps/leafcutter/app/_components/Footer.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
138
apps/leafcutter/app/_components/GettingStartedDialog.tsx
Normal file
138
apps/leafcutter/app/_components/GettingStartedDialog.tsx
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
45
apps/leafcutter/app/_components/HelpButton.tsx
Normal file
45
apps/leafcutter/app/_components/HelpButton.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { Button } from "@mui/material";
|
||||
import { QuestionMark as QuestionMarkIcon } from "@mui/icons-material";
|
||||
import { useAppContext } from "./AppProvider";
|
||||
|
||||
export const HelpButton: FC = () => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [helpActive, setHelpActive] = useState(false);
|
||||
const {
|
||||
colors: { leafcutterElectricBlue },
|
||||
} = useAppContext();
|
||||
const onClick = () => {
|
||||
if (helpActive) {
|
||||
router.push(pathname);
|
||||
} else {
|
||||
router.push("/?tooltip=welcome");
|
||||
}
|
||||
setHelpActive(!helpActive);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
backgroundColor: leafcutterElectricBlue,
|
||||
width: "40px",
|
||||
height: "40px",
|
||||
minWidth: "40px",
|
||||
p: 0,
|
||||
borderRadius: "500px",
|
||||
":hover": {
|
||||
backgroundColor: leafcutterElectricBlue,
|
||||
opacity: 0.8,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<QuestionMarkIcon width="30px" height="30px" htmlColor="white" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
100
apps/leafcutter/app/_components/Home.tsx
Normal file
100
apps/leafcutter/app/_components/Home.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
"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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
75
apps/leafcutter/app/_components/InternalLayout.tsx
Normal file
75
apps/leafcutter/app/_components/InternalLayout.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
"use client";
|
||||
|
||||
import { FC, PropsWithChildren } from "react";
|
||||
import getConfig from "next/config";
|
||||
import { Grid, Container } from "@mui/material";
|
||||
import CookieConsent from "react-cookie-consent";
|
||||
import { useCookies } from "react-cookie";
|
||||
import { TopNav } from "./TopNav";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { GettingStartedDialog } from "./GettingStartedDialog";
|
||||
import { useAppContext } from "./AppProvider";
|
||||
// import { Footer } from "./Footer";
|
||||
|
||||
type LayoutProps = PropsWithChildren<{
|
||||
embedded?: boolean;
|
||||
}>;
|
||||
|
||||
export const InternalLayout: FC<LayoutProps> = ({
|
||||
embedded = false,
|
||||
children,
|
||||
}: any) => {
|
||||
const [cookies, setCookie] = useCookies(["cookieConsent"]);
|
||||
const consentGranted = cookies.cookieConsent === "true";
|
||||
const {
|
||||
colors: {
|
||||
white,
|
||||
almostBlack,
|
||||
leafcutterElectricBlue,
|
||||
cdrLinkOrange,
|
||||
helpYellow,
|
||||
},
|
||||
} = useAppContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container direction="column">
|
||||
{!embedded && (
|
||||
<Grid item>
|
||||
<TopNav />
|
||||
</Grid>
|
||||
)}
|
||||
{!embedded && <Sidebar open />}
|
||||
<Grid
|
||||
item
|
||||
sx={{ mt: embedded ? 0 : "100px", ml: embedded ? 0 : "310px" }}
|
||||
>
|
||||
<Container sx={{ padding: 2 }}>{children}</Container>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{!consentGranted ? (
|
||||
<CookieConsent
|
||||
style={{
|
||||
zIndex: 1500,
|
||||
backgroundColor: helpYellow,
|
||||
color: almostBlack,
|
||||
borderTop: `1px solid ${leafcutterElectricBlue}`,
|
||||
}}
|
||||
onAccept={() => setCookie("cookieConsent", "true", { path: "/" })}
|
||||
buttonStyle={{
|
||||
borderRadius: 500,
|
||||
backgroundColor: cdrLinkOrange,
|
||||
color: white,
|
||||
textTransform: "uppercase",
|
||||
padding: "10px 20px",
|
||||
fontWeight: "bold",
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
Leafcutter uses cookies for core funtionality.
|
||||
</CookieConsent>
|
||||
) : null}
|
||||
<GettingStartedDialog />
|
||||
</>
|
||||
);
|
||||
};
|
||||
68
apps/leafcutter/app/_components/LanguageSelect.tsx
Normal file
68
apps/leafcutter/app/_components/LanguageSelect.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { IconButton, Menu, MenuItem, Box } from "@mui/material";
|
||||
import { KeyboardArrowDown as KeyboardArrowDownIcon } from "@mui/icons-material";
|
||||
import {
|
||||
usePopupState,
|
||||
bindTrigger,
|
||||
bindMenu,
|
||||
} from "material-ui-popup-state/hooks";
|
||||
import { useAppContext } from "./AppProvider";
|
||||
// import { Tooltip } from "./Tooltip";
|
||||
|
||||
export const LanguageSelect = () => {
|
||||
const {
|
||||
colors: { white, leafcutterElectricBlue },
|
||||
} = useAppContext();
|
||||
const router = useRouter();
|
||||
const locales: any = { en: "English", fr: "Français" };
|
||||
const locale = "en";
|
||||
const popupState = usePopupState({ variant: "popover", popupId: "language" });
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<IconButton
|
||||
{...bindTrigger(popupState)}
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
borderRadius: 500,
|
||||
color: white,
|
||||
backgroundColor: leafcutterElectricBlue,
|
||||
fontWeight: "bold",
|
||||
textTransform: "uppercase",
|
||||
pl: 4,
|
||||
pr: 3,
|
||||
":hover": {
|
||||
backgroundColor: leafcutterElectricBlue,
|
||||
opacity: 0.8,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{locales[locale as any] ?? locales.en}
|
||||
<KeyboardArrowDownIcon />
|
||||
</IconButton>
|
||||
<Menu {...bindMenu(popupState)}>
|
||||
{Object.keys(locales).map((locale) => (
|
||||
<MenuItem
|
||||
key={locale}
|
||||
onClick={() => {
|
||||
// router.push(router.route, router.route, { locale });
|
||||
popupState.close();
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: 130 }}>{locales[locale]}</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/* <Tooltip
|
||||
title={t("languageTooltipTitle")}
|
||||
description={t("languageTooltipDescription")}
|
||||
color="#fff"
|
||||
backgroundColor="#a5a6f6"
|
||||
placement="top"
|
||||
> </Tooltip> */
|
||||
24
apps/leafcutter/app/_components/LiveDataViewer.tsx
Normal file
24
apps/leafcutter/app/_components/LiveDataViewer.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"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} />;
|
||||
};
|
||||
150
apps/leafcutter/app/_components/MetricSelectCard.tsx
Normal file
150
apps/leafcutter/app/_components/MetricSelectCard.tsx
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
"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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
49
apps/leafcutter/app/_components/MultiProvider.tsx
Normal file
49
apps/leafcutter/app/_components/MultiProvider.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"use client";
|
||||
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { FC, PropsWithChildren } from "react";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { CssBaseline } from "@mui/material";
|
||||
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 { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
|
||||
import en from "app/_locales/en.json";
|
||||
import fr from "app/_locales/fr.json";
|
||||
import "@fontsource/poppins/400.css";
|
||||
import "@fontsource/poppins/700.css";
|
||||
import "@fontsource/roboto/400.css";
|
||||
import "@fontsource/roboto/700.css";
|
||||
import "@fontsource/playfair-display/900.css";
|
||||
import "app/_styles/global.css";
|
||||
import { LicenseInfo } from "@mui/x-date-pickers-pro";
|
||||
|
||||
LicenseInfo.setLicenseKey(
|
||||
"7c9bf25d9e240f76e77cbf7d2ba58a23Tz02NjU4OCxFPTE3MTU4NjIzMzQ2ODgsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI="
|
||||
);
|
||||
|
||||
const messages: any = { en, fr };
|
||||
|
||||
export const MultiProvider: FC<PropsWithChildren> = ({ children }: any) => {
|
||||
// const { locale = "en" } = useRouter();
|
||||
const locale = "en";
|
||||
|
||||
return (
|
||||
<NextAppDirEmotionCacheProvider options={{ key: "css" }}>
|
||||
<SessionProvider>
|
||||
<CookiesProvider>
|
||||
<CssBaseline />
|
||||
<AppProvider>
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||
<I18n locale={locale} messages={messages[locale]}>
|
||||
{children}
|
||||
</I18n>
|
||||
</LocalizationProvider>
|
||||
</AppProvider>
|
||||
</CookiesProvider>
|
||||
</SessionProvider>
|
||||
</NextAppDirEmotionCacheProvider>
|
||||
);
|
||||
};
|
||||
44
apps/leafcutter/app/_components/OpenSearchWrapper.tsx
Normal file
44
apps/leafcutter/app/_components/OpenSearchWrapper.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"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>
|
||||
);
|
||||
40
apps/leafcutter/app/_components/PageHeader.tsx
Normal file
40
apps/leafcutter/app/_components/PageHeader.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
251
apps/leafcutter/app/_components/QueryBuilder.tsx
Normal file
251
apps/leafcutter/app/_components/QueryBuilder.tsx
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
230
apps/leafcutter/app/_components/QueryBuilderSection.tsx
Normal file
230
apps/leafcutter/app/_components/QueryBuilderSection.tsx
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
118
apps/leafcutter/app/_components/QueryDateRangeSelector.tsx
Normal file
118
apps/leafcutter/app/_components/QueryDateRangeSelector.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
94
apps/leafcutter/app/_components/QueryListSelector.tsx
Normal file
94
apps/leafcutter/app/_components/QueryListSelector.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
126
apps/leafcutter/app/_components/QueryText.tsx
Normal file
126
apps/leafcutter/app/_components/QueryText.tsx
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
80
apps/leafcutter/app/_components/Question.tsx
Normal file
80
apps/leafcutter/app/_components/Question.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
86
apps/leafcutter/app/_components/RawDataViewer.tsx
Normal file
86
apps/leafcutter/app/_components/RawDataViewer.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
255
apps/leafcutter/app/_components/Sidebar.tsx
Normal file
255
apps/leafcutter/app/_components/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import DashboardMenuIcon from "images/dashboard-menu.png";
|
||||
import AboutMenuIcon from "images/about-menu.png";
|
||||
import TrendsMenuIcon from "images/trends-menu.png";
|
||||
import SearchCreateMenuIcon from "images/search-create-menu.png";
|
||||
import FAQMenuIcon from "images/faq-menu.png";
|
||||
import Image from "next/legacy/image";
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
Typography,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Drawer,
|
||||
} from "@mui/material";
|
||||
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 { ArrowCircleRight as ArrowCircleRightIcon } from "@mui/icons-material";
|
||||
|
||||
const MenuItem = ({
|
||||
name,
|
||||
href,
|
||||
selected,
|
||||
icon,
|
||||
iconSize,
|
||||
}: // tooltipTitle,
|
||||
// tooltipDescription,
|
||||
{
|
||||
name: string;
|
||||
href: string;
|
||||
selected: boolean;
|
||||
icon: any;
|
||||
iconSize: number;
|
||||
// tooltipTitle: string;
|
||||
// tooltipDescription: string;
|
||||
}) => {
|
||||
const {
|
||||
colors: { leafcutterLightBlue, black },
|
||||
} = useAppContext();
|
||||
|
||||
return (
|
||||
<Link href={href} passHref>
|
||||
<ListItem
|
||||
button
|
||||
sx={{
|
||||
paddingLeft: "62px",
|
||||
backgroundColor: selected ? leafcutterLightBlue : "transparent",
|
||||
mt: "6px",
|
||||
"& :hover": { backgroundColor: leafcutterLightBlue },
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
color: `${black} !important`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
ml: `${(20 - iconSize) / 2}px`,
|
||||
mr: `${(20 - iconSize) / 2}px`,
|
||||
}}
|
||||
>
|
||||
<Image src={icon} alt="" />
|
||||
</Box>
|
||||
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography
|
||||
variant="body1"
|
||||
style={{
|
||||
color: "#000 !important",
|
||||
fontSize: 14,
|
||||
fontFamily: "Roboto",
|
||||
fontWeight: 400,
|
||||
marginLeft: 10,
|
||||
marginTop: -3,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
</ListItem>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
interface SidebarProps {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
export const Sidebar: FC<SidebarProps> = ({ open }) => {
|
||||
const t = useTranslate();
|
||||
const pathname = usePathname();
|
||||
const section = pathname.split("/")[1];
|
||||
const {
|
||||
colors: { white }, // leafcutterElectricBlue, leafcutterLightBlue,
|
||||
} = useAppContext();
|
||||
|
||||
// const [recentUpdates, setRecentUpdates] = useState([]);
|
||||
|
||||
/*
|
||||
useEffect(() => {
|
||||
const getRecentUpdates = async () => {
|
||||
const result = await fetch(`/api/trends/recent`);
|
||||
const json = await result.json();
|
||||
setRecentUpdates(json);
|
||||
};
|
||||
getRecentUpdates();
|
||||
}, []);
|
||||
*/
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
sx={{ width: 300, flexShrink: 0 }}
|
||||
color="secondary"
|
||||
variant="permanent"
|
||||
anchor="left"
|
||||
open={open}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: 300,
|
||||
border: 0,
|
||||
mt: "90px",
|
||||
mb: "200px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="space-between"
|
||||
wrap="nowrap"
|
||||
sx={{ backgroundColor: white, height: "100%" }}
|
||||
>
|
||||
<Grid item container direction="column" sx={{ mt: "6px" }} flexGrow={1}>
|
||||
<Tooltip
|
||||
title={t("dashboardNavigationCardTitle")}
|
||||
description={t("dashboardNavigationCardDescription")}
|
||||
tooltipID="navigation"
|
||||
placement="right"
|
||||
nextURL="/?tooltip=recentUpdates"
|
||||
previousURL="/?tooltip=welcome"
|
||||
>
|
||||
<List component="nav">
|
||||
<MenuItem
|
||||
name={t("dashboardMenuItem")}
|
||||
href="/"
|
||||
icon={DashboardMenuIcon}
|
||||
iconSize={20}
|
||||
selected={section === ""}
|
||||
/>
|
||||
<MenuItem
|
||||
name={t("searchAndCreateMenuItem")}
|
||||
href="/create"
|
||||
icon={SearchCreateMenuIcon}
|
||||
iconSize={20}
|
||||
selected={section === "create"}
|
||||
/>
|
||||
<MenuItem
|
||||
name={t("trendsMenuItem")}
|
||||
href="/trends"
|
||||
icon={TrendsMenuIcon}
|
||||
iconSize={13}
|
||||
selected={section === "trends"}
|
||||
/>
|
||||
<MenuItem
|
||||
name={t("faqMenuItem")}
|
||||
href="/faq"
|
||||
icon={FAQMenuIcon}
|
||||
iconSize={20}
|
||||
selected={section === "faq"}
|
||||
/>
|
||||
<MenuItem
|
||||
name={t("aboutMenuItem")}
|
||||
href="/about"
|
||||
icon={AboutMenuIcon}
|
||||
iconSize={20}
|
||||
selected={section === "about"}
|
||||
/>
|
||||
</List>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
{/*
|
||||
<Tooltip
|
||||
title={t("recentUpdatesCardTitle")}
|
||||
description={t("recentUpdatesCardDescription")}
|
||||
tooltipID="recentUpdates"
|
||||
nextURL="/?tooltip=profile"
|
||||
previousURL="/?tooltip=navigation"
|
||||
placement="right"
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
backgroundColor: leafcutterLightBlue,
|
||||
height: 350,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
fontSize: 14,
|
||||
backgroundColor: leafcutterElectricBlue,
|
||||
color: white,
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
{t("recentUpdatesTitle")}
|
||||
</Typography>
|
||||
{recentUpdates.map((trend, index) => (
|
||||
<Link href={`/visualizations/${trend.id}`} passHref key={index}>
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
p: 2,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: leafcutterElectricBlue, fontWeight: 700 }}
|
||||
>
|
||||
{trend.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: leafcutterElectricBlue }}
|
||||
>
|
||||
{trend.description}{" "}
|
||||
<ArrowCircleRightIcon
|
||||
sx={{ height: 16, width: 16, mb: "-4px" }}
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Link>
|
||||
))}
|
||||
</Grid>
|
||||
</Tooltip> */}
|
||||
</Grid>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
160
apps/leafcutter/app/_components/Tooltip.tsx
Normal file
160
apps/leafcutter/app/_components/Tooltip.tsx
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
120
apps/leafcutter/app/_components/TopNav.tsx
Normal file
120
apps/leafcutter/app/_components/TopNav.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
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 { useAppContext } from "./AppProvider";
|
||||
// import { LanguageSelect } from "./LanguageSelect";
|
||||
|
||||
export const TopNav: FC = () => {
|
||||
const t = useTranslate();
|
||||
const {
|
||||
colors: { white, leafcutterElectricBlue, cdrLinkOrange },
|
||||
typography: { h5, h6 },
|
||||
} = useAppContext();
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
position="fixed"
|
||||
elevation={1}
|
||||
sx={{
|
||||
backgroundColor: white,
|
||||
marginBottom: 180,
|
||||
opacity: 0.95,
|
||||
pt: 2,
|
||||
pb: 2,
|
||||
pr: 3,
|
||||
pl: 6,
|
||||
backdropFilter: "blur(10px)",
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
direction="row"
|
||||
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>
|
||||
<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>
|
||||
</Grid>
|
||||
</Link>
|
||||
<Grid item>
|
||||
<HelpButton />
|
||||
</Grid>
|
||||
|
||||
{/* <Tooltip
|
||||
title={t("languageOptionsCardTitle")}
|
||||
description={t("languageOptionsCardDescription")}
|
||||
emoji="✍️"
|
||||
tooltipID="language"
|
||||
placement="bottom"
|
||||
>
|
||||
<Grid item>
|
||||
<LanguageSelect />
|
||||
</Grid>
|
||||
</Tooltip> */}
|
||||
<Tooltip
|
||||
title={t("profileSettingsCardTitle")}
|
||||
description={t("profileSettingsCardDescription")}
|
||||
tooltipID="profile"
|
||||
placement="bottom"
|
||||
previousURL="/?tooltip=recentUpdates"
|
||||
nextURL="/create?tooltip=searchCreate"
|
||||
>
|
||||
<Grid item>
|
||||
<AccountButton />
|
||||
</Grid>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</AppBar>
|
||||
);
|
||||
};
|
||||
373
apps/leafcutter/app/_components/VisualizationBuilder.tsx
Normal file
373
apps/leafcutter/app/_components/VisualizationBuilder.tsx
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
75
apps/leafcutter/app/_components/VisualizationCard.tsx
Normal file
75
apps/leafcutter/app/_components/VisualizationCard.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
"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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
45
apps/leafcutter/app/_components/VisualizationDetail.tsx
Normal file
45
apps/leafcutter/app/_components/VisualizationDetail.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
157
apps/leafcutter/app/_components/VisualizationDetailDialog.tsx
Normal file
157
apps/leafcutter/app/_components/VisualizationDetailDialog.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
110
apps/leafcutter/app/_components/VisualizationSelectCard.tsx
Normal file
110
apps/leafcutter/app/_components/VisualizationSelectCard.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
58
apps/leafcutter/app/_components/Welcome.tsx
Normal file
58
apps/leafcutter/app/_components/Welcome.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
"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>
|
||||
);
|
||||
};
|
||||
140
apps/leafcutter/app/_components/WelcomeDialog.tsx
Normal file
140
apps/leafcutter/app/_components/WelcomeDialog.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
"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'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'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>
|
||||
);
|
||||
};
|
||||
1386
apps/leafcutter/app/_config/taxonomy.json
Normal file
1386
apps/leafcutter/app/_config/taxonomy.json
Normal file
File diff suppressed because it is too large
Load diff
3239
apps/leafcutter/app/_config/unRegions.json
Normal file
3239
apps/leafcutter/app/_config/unRegions.json
Normal file
File diff suppressed because it is too large
Load diff
31
apps/leafcutter/app/_config/visualizationMap.json
Normal file
31
apps/leafcutter/app/_config/visualizationMap.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"visualizations": {
|
||||
"horizontalBar": {
|
||||
"name": "Horizontal Bar"
|
||||
},
|
||||
"verticalBar": {
|
||||
"name": "Vertical Bar"
|
||||
},
|
||||
"line": {
|
||||
"name": "Line"
|
||||
},
|
||||
"pieDonut": {
|
||||
"name": "Pie"
|
||||
},
|
||||
"dataTable": {
|
||||
"name": "Data Table"
|
||||
},
|
||||
"metric": {
|
||||
"name": "Metric"
|
||||
},
|
||||
"tagCloud": {
|
||||
"name": "Tag Cloud"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"incidentType": ["horizontalBar"],
|
||||
"targetedGroup": ["horizontalBar"],
|
||||
"impactedTechnology": ["horizontalBar"],
|
||||
"region": ["horizontalBar"]
|
||||
}
|
||||
}
|
||||
38
apps/leafcutter/app/_config/visualizations/dataTable.json
Normal file
38
apps/leafcutter/app/_config/visualizations/dataTable.json
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"title": "DataTable",
|
||||
"type": "table",
|
||||
"aggs": [
|
||||
{
|
||||
"id": "1",
|
||||
"enabled": true,
|
||||
"type": "count",
|
||||
"params": {},
|
||||
"schema": "metric"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"enabled": true,
|
||||
"type": "terms",
|
||||
"params": {
|
||||
"field": "incident.keyword",
|
||||
"orderBy": "1",
|
||||
"order": "desc",
|
||||
"size": 10,
|
||||
"otherBucket": false,
|
||||
"otherBucketLabel": "Other",
|
||||
"missingBucket": false,
|
||||
"missingBucketLabel": "Missing"
|
||||
},
|
||||
"schema": "bucket"
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
"perPage": 10,
|
||||
"showPartialRows": false,
|
||||
"showMetricsAtAllLevels": false,
|
||||
"sort": { "columnIndex": null, "direction": null },
|
||||
"showTotal": false,
|
||||
"totalFunc": "sum",
|
||||
"percentageCol": ""
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"title": "",
|
||||
"type": "horizontal_bar",
|
||||
"aggs": [
|
||||
{
|
||||
"id": "1",
|
||||
"enabled": true,
|
||||
"type": "count",
|
||||
"params": {},
|
||||
"schema": "metric"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"enabled": true,
|
||||
"type": "terms",
|
||||
"params": {
|
||||
"field": "incident.keyword",
|
||||
"orderBy": "1",
|
||||
"order": "desc",
|
||||
"size": 5,
|
||||
"otherBucket": true,
|
||||
"otherBucketLabel": "Other",
|
||||
"missingBucket": false,
|
||||
"missingBucketLabel": "Missing"
|
||||
},
|
||||
"schema": "segment"
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
"type": "histogram",
|
||||
"grid": { "categoryLines": false },
|
||||
"categoryAxes": [
|
||||
{
|
||||
"id": "CategoryAxis-1",
|
||||
"type": "category",
|
||||
"position": "left",
|
||||
"show": true,
|
||||
"style": {},
|
||||
"scale": { "type": "linear" },
|
||||
"labels": {
|
||||
"show": true,
|
||||
"rotate": 0,
|
||||
"filter": false,
|
||||
"truncate": 200
|
||||
},
|
||||
"title": {}
|
||||
}
|
||||
],
|
||||
"valueAxes": [
|
||||
{
|
||||
"id": "ValueAxis-1",
|
||||
"name": "LeftAxis-1",
|
||||
"type": "value",
|
||||
"position": "bottom",
|
||||
"show": true,
|
||||
"style": {},
|
||||
"scale": { "type": "linear", "mode": "normal" },
|
||||
"labels": {
|
||||
"show": true,
|
||||
"rotate": 75,
|
||||
"filter": true,
|
||||
"truncate": 100
|
||||
},
|
||||
"title": { "text": "Count" }
|
||||
}
|
||||
],
|
||||
"seriesParams": [
|
||||
{
|
||||
"show": true,
|
||||
"type": "histogram",
|
||||
"mode": "normal",
|
||||
"data": { "label": "Count", "id": "1" },
|
||||
"valueAxis": "ValueAxis-1",
|
||||
"drawLinesBetweenPoints": true,
|
||||
"lineWidth": 2,
|
||||
"showCircles": true
|
||||
}
|
||||
],
|
||||
"addTooltip": true,
|
||||
"addLegend": true,
|
||||
"legendPosition": "right",
|
||||
"times": [],
|
||||
"addTimeMarker": false,
|
||||
"labels": {},
|
||||
"thresholdLine": {
|
||||
"show": false,
|
||||
"value": 10,
|
||||
"width": 1,
|
||||
"style": "full",
|
||||
"color": "#E7664C"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"title": "BarStacked",
|
||||
"type": "horizontal_bar",
|
||||
"aggs": [
|
||||
{
|
||||
"id": "1",
|
||||
"enabled": true,
|
||||
"type": "count",
|
||||
"params": {},
|
||||
"schema": "metric"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"enabled": true,
|
||||
"type": "terms",
|
||||
"params": {
|
||||
"field": "actor.keyword",
|
||||
"orderBy": "_key",
|
||||
"order": "desc",
|
||||
"size": 5,
|
||||
"otherBucket": false,
|
||||
"otherBucketLabel": "Other",
|
||||
"missingBucket": false,
|
||||
"missingBucketLabel": "Missing"
|
||||
},
|
||||
"schema": "split"
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
"type": "histogram",
|
||||
"grid": { "categoryLines": false },
|
||||
"categoryAxes": [
|
||||
{
|
||||
"id": "CategoryAxis-1",
|
||||
"type": "category",
|
||||
"position": "left",
|
||||
"show": true,
|
||||
"style": {},
|
||||
"scale": { "type": "linear" },
|
||||
"labels": {
|
||||
"show": true,
|
||||
"rotate": 0,
|
||||
"filter": false,
|
||||
"truncate": 200
|
||||
},
|
||||
"title": {}
|
||||
}
|
||||
],
|
||||
"valueAxes": [
|
||||
{
|
||||
"id": "ValueAxis-1",
|
||||
"name": "LeftAxis-1",
|
||||
"type": "value",
|
||||
"position": "bottom",
|
||||
"show": true,
|
||||
"style": {},
|
||||
"scale": { "type": "linear", "mode": "normal" },
|
||||
"labels": {
|
||||
"show": true,
|
||||
"rotate": 75,
|
||||
"filter": true,
|
||||
"truncate": 100
|
||||
},
|
||||
"title": { "text": "Count" }
|
||||
}
|
||||
],
|
||||
"seriesParams": [
|
||||
{
|
||||
"show": true,
|
||||
"type": "histogram",
|
||||
"mode": "stacked",
|
||||
"data": { "label": "Count", "id": "1" },
|
||||
"valueAxis": "ValueAxis-1",
|
||||
"drawLinesBetweenPoints": true,
|
||||
"lineWidth": 2,
|
||||
"showCircles": true
|
||||
}
|
||||
],
|
||||
"addTooltip": true,
|
||||
"addLegend": true,
|
||||
"legendPosition": "right",
|
||||
"times": [],
|
||||
"addTimeMarker": false,
|
||||
"labels": {},
|
||||
"thresholdLine": {
|
||||
"show": false,
|
||||
"value": 10,
|
||||
"width": 1,
|
||||
"style": "full",
|
||||
"color": "#E7664C"
|
||||
},
|
||||
"row": true
|
||||
}
|
||||
}
|
||||
89
apps/leafcutter/app/_config/visualizations/line.json
Normal file
89
apps/leafcutter/app/_config/visualizations/line.json
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
{
|
||||
"title": "Line",
|
||||
"type": "line",
|
||||
"aggs": [
|
||||
{
|
||||
"id": "1",
|
||||
"enabled": true,
|
||||
"type": "count",
|
||||
"params": {},
|
||||
"schema": "metric"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"enabled": true,
|
||||
"type": "terms",
|
||||
"params": {
|
||||
"field": "technology.keyword",
|
||||
"orderBy": "1",
|
||||
"order": "desc",
|
||||
"size": 5,
|
||||
"otherBucket": false,
|
||||
"otherBucketLabel": "Other",
|
||||
"missingBucket": false,
|
||||
"missingBucketLabel": "Missing"
|
||||
},
|
||||
"schema": "segment"
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
"type": "line",
|
||||
"grid": { "categoryLines": false },
|
||||
"categoryAxes": [
|
||||
{
|
||||
"id": "CategoryAxis-1",
|
||||
"type": "category",
|
||||
"position": "bottom",
|
||||
"show": true,
|
||||
"style": {},
|
||||
"scale": { "type": "linear" },
|
||||
"labels": { "show": true, "filter": true, "truncate": 100 },
|
||||
"title": {}
|
||||
}
|
||||
],
|
||||
"valueAxes": [
|
||||
{
|
||||
"id": "ValueAxis-1",
|
||||
"name": "LeftAxis-1",
|
||||
"type": "value",
|
||||
"position": "left",
|
||||
"show": true,
|
||||
"style": {},
|
||||
"scale": { "type": "linear", "mode": "normal" },
|
||||
"labels": {
|
||||
"show": true,
|
||||
"rotate": 0,
|
||||
"filter": false,
|
||||
"truncate": 100
|
||||
},
|
||||
"title": { "text": "Count" }
|
||||
}
|
||||
],
|
||||
"seriesParams": [
|
||||
{
|
||||
"show": true,
|
||||
"type": "line",
|
||||
"mode": "normal",
|
||||
"data": { "label": "Count", "id": "1" },
|
||||
"valueAxis": "ValueAxis-1",
|
||||
"drawLinesBetweenPoints": true,
|
||||
"lineWidth": 2,
|
||||
"interpolate": "linear",
|
||||
"showCircles": true
|
||||
}
|
||||
],
|
||||
"addTooltip": true,
|
||||
"addLegend": true,
|
||||
"legendPosition": "right",
|
||||
"times": [],
|
||||
"addTimeMarker": false,
|
||||
"labels": {},
|
||||
"thresholdLine": {
|
||||
"show": false,
|
||||
"value": 10,
|
||||
"width": 1,
|
||||
"style": "full",
|
||||
"color": "#E7664C"
|
||||
}
|
||||
}
|
||||
}
|
||||
105
apps/leafcutter/app/_config/visualizations/lineStacked.json
Normal file
105
apps/leafcutter/app/_config/visualizations/lineStacked.json
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
{
|
||||
"title": "LineStacked",
|
||||
"type": "line",
|
||||
"aggs": [
|
||||
{
|
||||
"id": "1",
|
||||
"enabled": true,
|
||||
"type": "count",
|
||||
"params": {},
|
||||
"schema": "metric"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"enabled": true,
|
||||
"type": "terms",
|
||||
"params": {
|
||||
"field": "technology.keyword",
|
||||
"orderBy": "1",
|
||||
"order": "desc",
|
||||
"size": 5,
|
||||
"otherBucket": false,
|
||||
"otherBucketLabel": "Other",
|
||||
"missingBucket": false,
|
||||
"missingBucketLabel": "Missing"
|
||||
},
|
||||
"schema": "segment"
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
"type": "line",
|
||||
"grid": { "categoryLines": false },
|
||||
"categoryAxes": [
|
||||
{
|
||||
"id": "CategoryAxis-1",
|
||||
"type": "category",
|
||||
"position": "bottom",
|
||||
"show": true,
|
||||
"style": {},
|
||||
"scale": { "type": "linear" },
|
||||
"labels": { "show": true, "filter": true, "truncate": 100 },
|
||||
"title": {}
|
||||
}
|
||||
],
|
||||
"valueAxes": [
|
||||
{
|
||||
"id": "ValueAxis-1",
|
||||
"name": "LeftAxis-1",
|
||||
"type": "value",
|
||||
"position": "left",
|
||||
"show": true,
|
||||
"style": {},
|
||||
"scale": { "type": "linear", "mode": "normal" },
|
||||
"labels": {
|
||||
"show": true,
|
||||
"rotate": 0,
|
||||
"filter": false,
|
||||
"truncate": 100
|
||||
},
|
||||
"title": { "text": "Count" }
|
||||
},
|
||||
{
|
||||
"id": "ValueAxis-2",
|
||||
"name": "RightAxis-1",
|
||||
"type": "value",
|
||||
"position": "right",
|
||||
"show": true,
|
||||
"style": {},
|
||||
"scale": { "type": "linear", "mode": "normal" },
|
||||
"labels": {
|
||||
"show": true,
|
||||
"rotate": 0,
|
||||
"filter": false,
|
||||
"truncate": 100
|
||||
},
|
||||
"title": { "text": "Count" }
|
||||
}
|
||||
],
|
||||
"seriesParams": [
|
||||
{
|
||||
"show": true,
|
||||
"type": "line",
|
||||
"mode": "stacked",
|
||||
"data": { "label": "Count", "id": "1" },
|
||||
"valueAxis": "ValueAxis-2",
|
||||
"drawLinesBetweenPoints": true,
|
||||
"lineWidth": 2,
|
||||
"interpolate": "step-after",
|
||||
"showCircles": true
|
||||
}
|
||||
],
|
||||
"addTooltip": true,
|
||||
"addLegend": true,
|
||||
"legendPosition": "right",
|
||||
"times": [],
|
||||
"addTimeMarker": false,
|
||||
"labels": {},
|
||||
"thresholdLine": {
|
||||
"show": false,
|
||||
"value": 10,
|
||||
"width": 1,
|
||||
"style": "full",
|
||||
"color": "#E7664C"
|
||||
}
|
||||
}
|
||||
}
|
||||
50
apps/leafcutter/app/_config/visualizations/metric.json
Normal file
50
apps/leafcutter/app/_config/visualizations/metric.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"title": "Metric",
|
||||
"type": "metric",
|
||||
"aggs": [
|
||||
{
|
||||
"id": "1",
|
||||
"enabled": true,
|
||||
"type": "count",
|
||||
"params": { "customLabel": "#" },
|
||||
"schema": "metric"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"enabled": true,
|
||||
"type": "terms",
|
||||
"params": {
|
||||
"field": "technology.keyword",
|
||||
"orderBy": "1",
|
||||
"order": "desc",
|
||||
"size": 5,
|
||||
"otherBucket": false,
|
||||
"otherBucketLabel": "Other",
|
||||
"missingBucket": false,
|
||||
"missingBucketLabel": "Missing"
|
||||
},
|
||||
"schema": "group"
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
"addTooltip": true,
|
||||
"addLegend": false,
|
||||
"type": "metric",
|
||||
"metric": {
|
||||
"percentageMode": false,
|
||||
"useRanges": false,
|
||||
"colorSchema": "Green to Red",
|
||||
"metricColorMode": "None",
|
||||
"colorsRange": [{ "from": 0, "to": 10000 }],
|
||||
"labels": { "show": true },
|
||||
"invertColors": false,
|
||||
"style": {
|
||||
"bgFill": "#000",
|
||||
"bgColor": false,
|
||||
"labelColor": false,
|
||||
"subText": "",
|
||||
"fontSize": 60
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
apps/leafcutter/app/_config/visualizations/pieDonut.json
Normal file
42
apps/leafcutter/app/_config/visualizations/pieDonut.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"title": "Pie",
|
||||
"type": "pie",
|
||||
"aggs": [
|
||||
{
|
||||
"id": "1",
|
||||
"enabled": true,
|
||||
"type": "count",
|
||||
"params": {},
|
||||
"schema": "metric"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"enabled": true,
|
||||
"type": "terms",
|
||||
"params": {
|
||||
"field": "technology.keyword",
|
||||
"orderBy": "1",
|
||||
"order": "desc",
|
||||
"size": 5,
|
||||
"otherBucket": true,
|
||||
"otherBucketLabel": "Other",
|
||||
"missingBucket": false,
|
||||
"missingBucketLabel": "Missing"
|
||||
},
|
||||
"schema": "segment"
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
"type": "pie",
|
||||
"addTooltip": true,
|
||||
"addLegend": true,
|
||||
"legendPosition": "right",
|
||||
"isDonut": true,
|
||||
"labels": {
|
||||
"show": false,
|
||||
"values": true,
|
||||
"last_level": true,
|
||||
"truncate": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
36
apps/leafcutter/app/_config/visualizations/tagCloud.json
Normal file
36
apps/leafcutter/app/_config/visualizations/tagCloud.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"title": "Cloud",
|
||||
"type": "tagcloud",
|
||||
"aggs": [
|
||||
{
|
||||
"id": "1",
|
||||
"enabled": true,
|
||||
"type": "count",
|
||||
"params": {},
|
||||
"schema": "metric"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"enabled": true,
|
||||
"type": "terms",
|
||||
"params": {
|
||||
"field": "incident.keyword",
|
||||
"orderBy": "1",
|
||||
"order": "desc",
|
||||
"size": 5,
|
||||
"otherBucket": false,
|
||||
"otherBucketLabel": "Other",
|
||||
"missingBucket": false,
|
||||
"missingBucketLabel": "Missing"
|
||||
},
|
||||
"schema": "segment"
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
"scale": "linear",
|
||||
"orientation": "single",
|
||||
"minFontSize": 18,
|
||||
"maxFontSize": 72,
|
||||
"showLabel": true
|
||||
}
|
||||
}
|
||||
88
apps/leafcutter/app/_config/visualizations/verticalBar.json
Normal file
88
apps/leafcutter/app/_config/visualizations/verticalBar.json
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
{
|
||||
"title": "VerticalBar",
|
||||
"type": "histogram",
|
||||
"aggs": [
|
||||
{
|
||||
"id": "1",
|
||||
"enabled": true,
|
||||
"type": "count",
|
||||
"params": {},
|
||||
"schema": "metric"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"enabled": true,
|
||||
"type": "terms",
|
||||
"params": {
|
||||
"field": "actor.keyword",
|
||||
"orderBy": "1",
|
||||
"order": "desc",
|
||||
"size": 10,
|
||||
"otherBucket": false,
|
||||
"otherBucketLabel": "Other",
|
||||
"missingBucket": false,
|
||||
"missingBucketLabel": "Missing"
|
||||
},
|
||||
"schema": "segment"
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
"type": "histogram",
|
||||
"grid": { "categoryLines": false },
|
||||
"categoryAxes": [
|
||||
{
|
||||
"id": "CategoryAxis-1",
|
||||
"type": "category",
|
||||
"position": "bottom",
|
||||
"show": true,
|
||||
"style": {},
|
||||
"scale": { "type": "linear" },
|
||||
"labels": { "show": true, "filter": true, "truncate": 100 },
|
||||
"title": {}
|
||||
}
|
||||
],
|
||||
"valueAxes": [
|
||||
{
|
||||
"id": "ValueAxis-1",
|
||||
"name": "LeftAxis-1",
|
||||
"type": "value",
|
||||
"position": "left",
|
||||
"show": true,
|
||||
"style": {},
|
||||
"scale": { "type": "linear", "mode": "normal" },
|
||||
"labels": {
|
||||
"show": true,
|
||||
"rotate": 0,
|
||||
"filter": false,
|
||||
"truncate": 100
|
||||
},
|
||||
"title": { "text": "Count" }
|
||||
}
|
||||
],
|
||||
"seriesParams": [
|
||||
{
|
||||
"show": true,
|
||||
"type": "histogram",
|
||||
"mode": "normal",
|
||||
"data": { "label": "Count", "id": "1" },
|
||||
"valueAxis": "ValueAxis-1",
|
||||
"drawLinesBetweenPoints": true,
|
||||
"lineWidth": 2,
|
||||
"showCircles": true
|
||||
}
|
||||
],
|
||||
"addTooltip": true,
|
||||
"addLegend": true,
|
||||
"legendPosition": "right",
|
||||
"times": [],
|
||||
"addTimeMarker": false,
|
||||
"labels": { "show": false },
|
||||
"thresholdLine": {
|
||||
"show": false,
|
||||
"value": 10,
|
||||
"width": 1,
|
||||
"style": "full",
|
||||
"color": "#E7664C"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
{
|
||||
"title": "VerticalBarStacked",
|
||||
"type": "histogram",
|
||||
"aggs": [
|
||||
{
|
||||
"id": "1",
|
||||
"enabled": true,
|
||||
"type": "count",
|
||||
"params": {},
|
||||
"schema": "metric"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"enabled": true,
|
||||
"type": "terms",
|
||||
"params": {
|
||||
"field": "incident.keyword",
|
||||
"orderBy": "1",
|
||||
"order": "desc",
|
||||
"size": 5,
|
||||
"otherBucket": false,
|
||||
"otherBucketLabel": "Other",
|
||||
"missingBucket": false,
|
||||
"missingBucketLabel": "Missing"
|
||||
},
|
||||
"schema": "segment"
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
"type": "histogram",
|
||||
"grid": { "categoryLines": false },
|
||||
"categoryAxes": [
|
||||
{
|
||||
"id": "CategoryAxis-1",
|
||||
"type": "category",
|
||||
"position": "bottom",
|
||||
"show": true,
|
||||
"style": {},
|
||||
"scale": { "type": "linear" },
|
||||
"labels": { "show": true, "filter": true, "truncate": 100 },
|
||||
"title": {}
|
||||
}
|
||||
],
|
||||
"valueAxes": [
|
||||
{
|
||||
"id": "ValueAxis-1",
|
||||
"name": "LeftAxis-1",
|
||||
"type": "value",
|
||||
"position": "left",
|
||||
"show": true,
|
||||
"style": {},
|
||||
"scale": { "type": "linear", "mode": "normal" },
|
||||
"labels": {
|
||||
"show": true,
|
||||
"rotate": 0,
|
||||
"filter": false,
|
||||
"truncate": 100
|
||||
},
|
||||
"title": { "text": "Count" }
|
||||
}
|
||||
],
|
||||
"seriesParams": [
|
||||
{
|
||||
"show": true,
|
||||
"type": "histogram",
|
||||
"mode": "stacked",
|
||||
"data": { "label": "Count", "id": "1" },
|
||||
"valueAxis": "ValueAxis-1",
|
||||
"drawLinesBetweenPoints": true,
|
||||
"lineWidth": 2,
|
||||
"showCircles": true
|
||||
}
|
||||
],
|
||||
"addTooltip": true,
|
||||
"addLegend": true,
|
||||
"legendPosition": "right",
|
||||
"times": [],
|
||||
"addTimeMarker": false,
|
||||
"labels": { "show": false },
|
||||
"thresholdLine": {
|
||||
"show": false,
|
||||
"value": 10,
|
||||
"width": 1,
|
||||
"style": "full",
|
||||
"color": "#E7664C"
|
||||
}
|
||||
}
|
||||
}
|
||||
17
apps/leafcutter/app/_lib/auth.ts
Normal file
17
apps/leafcutter/app/_lib/auth.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import type { NextAuthOptions } from "next-auth";
|
||||
import Google from "next-auth/providers/google";
|
||||
import Apple from "next-auth/providers/apple";
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
Google({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID ?? "",
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? "",
|
||||
}),
|
||||
Apple({
|
||||
clientId: process.env.APPLE_CLIENT_ID ?? "",
|
||||
clientSecret: process.env.APPLE_CLIENT_SECRET ?? "",
|
||||
}),
|
||||
],
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
};
|
||||
577
apps/leafcutter/app/_lib/opensearch.ts
Normal file
577
apps/leafcutter/app/_lib/opensearch.ts
Normal file
|
|
@ -0,0 +1,577 @@
|
|||
/* eslint-disable no-underscore-dangle */
|
||||
import { Client } from "@opensearch-project/opensearch";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
/* Common */
|
||||
|
||||
const globalIndex = ".kibana_1";
|
||||
const dataIndexName = "sample_tagged_tickets";
|
||||
const userMetadataIndexName = "user_metadata";
|
||||
|
||||
// const baseURL = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
|
||||
|
||||
const baseURL = `https://localhost:9200`;
|
||||
|
||||
const createClient = () => new Client({
|
||||
node: baseURL,
|
||||
auth: {
|
||||
username: process.env.OPENSEARCH_USERNAME!,
|
||||
password: process.env.OPENSEARCH_PASSWORD!,
|
||||
},
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
|
||||
const getDocumentID = (doc: any) => doc._id.split(":")[1];
|
||||
|
||||
const getEmbedURL = (tenant: string, visualizationID: string) =>
|
||||
`/app/visualize?security_tenant=${tenant}#/edit/${visualizationID}?embed=true`;
|
||||
|
||||
export const getVisualization = async (id: string) => {
|
||||
const client = createClient();
|
||||
const res = await client.get({
|
||||
id: `visualization:${id}`,
|
||||
index: globalIndex,
|
||||
});
|
||||
|
||||
return res.body._source;
|
||||
};
|
||||
|
||||
const generateKuery = (searchQuery: any) => {
|
||||
const searchTemplate = {
|
||||
query: {
|
||||
query: "",
|
||||
language: "kuery",
|
||||
},
|
||||
filter: [],
|
||||
indexRefName: "kibanaSavedObjectMeta.searchSourceJSON.index",
|
||||
};
|
||||
const incidentTypeClause = searchQuery.incidentType.values
|
||||
.map((value: string) => `incident:${value} `)
|
||||
.join(" or ");
|
||||
const allTechnologies = [
|
||||
...searchQuery.platform.values,
|
||||
...searchQuery.device.values,
|
||||
...searchQuery.service.values,
|
||||
...searchQuery.maker.values,
|
||||
];
|
||||
const technologyClause = allTechnologies
|
||||
.map((value: string) => `technology:${value} `)
|
||||
.join(" or ");
|
||||
const targetedGroupClause = searchQuery.targetedGroup.values
|
||||
.map((value: string) => `targeted_group:${value} `)
|
||||
.join(" or ");
|
||||
const countryClause = searchQuery.country.values
|
||||
.map((value: string) => `country:${value} `)
|
||||
.join(" or ");
|
||||
const subregionClause = searchQuery.subregion.values
|
||||
.map((value: string) => `region:${value} `)
|
||||
.join(" or ");
|
||||
const continentClause = searchQuery.continent.values
|
||||
.map((value: string) => `continent:${value} `)
|
||||
.join(" or ");
|
||||
const kueryString = [
|
||||
incidentTypeClause,
|
||||
technologyClause,
|
||||
targetedGroupClause,
|
||||
countryClause,
|
||||
subregionClause,
|
||||
continentClause,
|
||||
]
|
||||
.filter((clause) => clause !== "")
|
||||
.join(" and ");
|
||||
searchTemplate.query.query = kueryString;
|
||||
|
||||
return JSON.stringify(searchTemplate);
|
||||
};
|
||||
|
||||
export const getUserMetadata = async (username: string) => {
|
||||
const client = createClient();
|
||||
let res: any;
|
||||
|
||||
try {
|
||||
res = await client.get({
|
||||
id: username,
|
||||
index: userMetadataIndexName,
|
||||
});
|
||||
} catch (e) {
|
||||
await client.create({
|
||||
id: username,
|
||||
index: userMetadataIndexName,
|
||||
body: { username, savedSearches: [] }
|
||||
});
|
||||
|
||||
res = await client.get({
|
||||
id: username,
|
||||
index: userMetadataIndexName,
|
||||
});
|
||||
}
|
||||
|
||||
return res?.body._source;
|
||||
};
|
||||
|
||||
export const saveUserMetadata = async (username: string, metadata: any) => {
|
||||
const client = createClient();
|
||||
await client.update({
|
||||
id: username,
|
||||
index: userMetadataIndexName,
|
||||
body: { doc: { username, ...metadata } }
|
||||
});
|
||||
};
|
||||
|
||||
/* User */
|
||||
|
||||
const getCurrentUserIndex = async (email: string) => {
|
||||
const userIndexName = email.replace(/[\W\d_]/g, "").toLowerCase();
|
||||
const client = createClient();
|
||||
const aliasesResponse = await client.indices.getAlias({
|
||||
name: `.kibana_*_${userIndexName}`,
|
||||
});
|
||||
|
||||
// prefer alias if it exists
|
||||
if (Object.keys(aliasesResponse.body).length > 0) {
|
||||
return Object.keys(aliasesResponse.body)[0];
|
||||
}
|
||||
|
||||
const indicesResponse = await client.indices.get({
|
||||
index: `.kibana_*_${userIndexName}_1`,
|
||||
});
|
||||
const currentUserIndex = Object.keys(indicesResponse.body)[0];
|
||||
|
||||
return currentUserIndex;
|
||||
};
|
||||
|
||||
const getIndexPattern: any = async (index: string) => {
|
||||
const client = createClient();
|
||||
const query = {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{ match: { type: "index-pattern" } },
|
||||
{
|
||||
match: {
|
||||
"index-pattern.title": dataIndexName,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const res = await client.search({
|
||||
index,
|
||||
size: 1,
|
||||
body: query,
|
||||
sort: ["updated_at:desc"],
|
||||
});
|
||||
|
||||
if (res.body.hits.total.value === 0) {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
return createCurrentUserIndexPattern(index);
|
||||
}
|
||||
|
||||
const {
|
||||
hits: {
|
||||
hits: [indexPattern],
|
||||
},
|
||||
} = res.body;
|
||||
|
||||
return indexPattern;
|
||||
};
|
||||
|
||||
const createCurrentUserIndexPattern = async (index: string) => {
|
||||
const { _source: globalIndexPattern } = await getIndexPattern(globalIndex);
|
||||
globalIndexPattern.updated_at = new Date().toISOString();
|
||||
|
||||
const id = uuid();
|
||||
const fullID = `index-pattern:${id}`;
|
||||
const client = createClient();
|
||||
const res = await client.create({
|
||||
id: fullID,
|
||||
index,
|
||||
refresh: true,
|
||||
body: globalIndexPattern,
|
||||
});
|
||||
|
||||
return res.body;
|
||||
};
|
||||
|
||||
const getIndexPatternID = async (index: string) => {
|
||||
const indexPattern = await getIndexPattern(index);
|
||||
return getDocumentID(indexPattern);
|
||||
};
|
||||
|
||||
interface createUserVisualizationProps {
|
||||
email: string;
|
||||
query: any;
|
||||
visualizationID: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const createUserVisualization = async (
|
||||
props: createUserVisualizationProps
|
||||
) => {
|
||||
const { email, query, visualizationID, title, description } = props;
|
||||
const userIndex = await getCurrentUserIndex(email);
|
||||
const indexPatternID = await getIndexPatternID(userIndex);
|
||||
const id = uuid();
|
||||
const fullID = `visualization:${id}`;
|
||||
|
||||
const template: any = await getVisualization(visualizationID);
|
||||
template.visualization.title = title;
|
||||
template.visualization.description = description;
|
||||
template.visualization.kibanaSavedObjectMeta.searchSourceJSON =
|
||||
generateKuery(query);
|
||||
template.references = [
|
||||
{
|
||||
name: "kibanaSavedObjectMeta.searchSourceJSON.index",
|
||||
type: "index-pattern",
|
||||
id: indexPatternID,
|
||||
},
|
||||
];
|
||||
template.updated_at = new Date().toISOString();
|
||||
|
||||
const client = createClient();
|
||||
const res = await client.create({
|
||||
id: fullID,
|
||||
index: userIndex,
|
||||
refresh: true,
|
||||
body: template,
|
||||
});
|
||||
|
||||
return getDocumentID(res.body);
|
||||
};
|
||||
|
||||
export const getUserVisualization = async (email: string, id: string) => {
|
||||
const userIndex = await getCurrentUserIndex(email);
|
||||
const client = createClient();
|
||||
const res = await client.get({
|
||||
id: `visualization:${id}`,
|
||||
index: userIndex,
|
||||
});
|
||||
|
||||
return res.body;
|
||||
};
|
||||
|
||||
interface updateVisualizationProps {
|
||||
email: string;
|
||||
id: string;
|
||||
query: any;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const updateUserVisualization = async (
|
||||
props: updateVisualizationProps
|
||||
) => {
|
||||
const { email, id, query, title, description } = props;
|
||||
const userIndex = await getCurrentUserIndex(email);
|
||||
const result: any = await getUserVisualization(email, id);
|
||||
const body = {
|
||||
doc: result._source,
|
||||
};
|
||||
body.doc.visualization.title = title;
|
||||
body.doc.visualization.description = description;
|
||||
body.doc.visualization.kibanaSavedObjectMeta.searchSourceJSON =
|
||||
generateKuery(query);
|
||||
|
||||
const client = createClient();
|
||||
try {
|
||||
await client.update({
|
||||
id: `visualization:${id}`,
|
||||
index: userIndex,
|
||||
body,
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log({ e });
|
||||
}
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
export const deleteUserVisualization = async (email: string, id: string) => {
|
||||
const userIndex = await getCurrentUserIndex(email);
|
||||
const client = createClient();
|
||||
client.delete({
|
||||
id: `visualization:${id}`,
|
||||
index: userIndex,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUserVisualizations = async (email: string, limit: number) => {
|
||||
const userIndex = await getCurrentUserIndex(email);
|
||||
const client = createClient();
|
||||
const query = {
|
||||
query: {
|
||||
match: { type: "visualization" },
|
||||
},
|
||||
};
|
||||
const res = await client.search({
|
||||
index: userIndex,
|
||||
size: limit,
|
||||
body: query,
|
||||
sort: ["updated_at:desc"],
|
||||
});
|
||||
const {
|
||||
hits: { hits },
|
||||
} = res.body;
|
||||
const results = hits.map((hit: any) => ({
|
||||
id: getDocumentID(hit),
|
||||
title: hit._source.visualization.title,
|
||||
description: hit._source.visualization.description ?? "",
|
||||
url: getEmbedURL("private", getDocumentID(hit)),
|
||||
}));
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
/* Global */
|
||||
|
||||
export const performQuery = async (searchQuery: any, limit: number) => {
|
||||
const client = createClient();
|
||||
const body = {
|
||||
query: {
|
||||
bool: {
|
||||
must: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (searchQuery.relativeDate.values.length > 0) {
|
||||
searchQuery.relativeDate.values.forEach((value: string) => {
|
||||
// @ts-expect-error
|
||||
body.query.bool.must.push({
|
||||
range: {
|
||||
date: {
|
||||
gte: `now-${value}d`,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (searchQuery.startDate.values.length > 0) {
|
||||
searchQuery.startDate.values.forEach((value: string) => {
|
||||
// @ts-expect-error
|
||||
body.query.bool.must.push({
|
||||
range: {
|
||||
date: {
|
||||
gte: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (searchQuery.endDate.values.length > 0) {
|
||||
searchQuery.endDate.values.forEach((value: string) => {
|
||||
// @ts-expect-error
|
||||
body.query.bool.must.push({
|
||||
range: {
|
||||
date: {
|
||||
lte: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (searchQuery.incidentType.values.length > 0) {
|
||||
// @ts-expect-error
|
||||
body.query.bool.must.push({
|
||||
terms: { "incident.keyword": searchQuery.incidentType.values },
|
||||
});
|
||||
}
|
||||
|
||||
if (searchQuery.targetedGroup.values.length > 0) {
|
||||
// @ts-expect-error
|
||||
body.query.bool.must.push({
|
||||
terms: { "targeted_group.keyword": searchQuery.targetedGroup.values },
|
||||
});
|
||||
}
|
||||
|
||||
if (searchQuery.platform.values.length > 0) {
|
||||
// @ts-expect-error
|
||||
body.query.bool.must.push({
|
||||
terms: { "technology.keyword": searchQuery.platform.values },
|
||||
});
|
||||
}
|
||||
|
||||
if (searchQuery.device.values.length > 0) {
|
||||
// @ts-expect-error
|
||||
body.query.bool.must.push({
|
||||
terms: { "technology.keyword": searchQuery.device.values },
|
||||
});
|
||||
}
|
||||
|
||||
if (searchQuery.service.values.length > 0) {
|
||||
// @ts-expect-error
|
||||
body.query.bool.must.push({
|
||||
terms: { "technology.keyword": searchQuery.service.values },
|
||||
});
|
||||
}
|
||||
|
||||
if (searchQuery.maker.values.length > 0) {
|
||||
// @ts-expect-error
|
||||
body.query.bool.must.push({
|
||||
terms: { "technology.keyword": searchQuery.maker.values },
|
||||
});
|
||||
}
|
||||
|
||||
if (searchQuery.subregion.values.length > 0) {
|
||||
// @ts-expect-error
|
||||
body.query.bool.must.push({
|
||||
terms: { "region.keyword": searchQuery.subregion.values },
|
||||
});
|
||||
}
|
||||
|
||||
if (searchQuery.country.values.length > 0) {
|
||||
// @ts-expect-error
|
||||
body.query.bool.must.push({
|
||||
terms: { "country.keyword": searchQuery.country.values },
|
||||
});
|
||||
}
|
||||
|
||||
if (searchQuery.continent.values.length > 0) {
|
||||
// @ts-expect-error
|
||||
body.query.bool.must.push({
|
||||
terms: { "continent.keyword": searchQuery.continent.values },
|
||||
});
|
||||
}
|
||||
|
||||
const dataResponse = await client.search({
|
||||
index: dataIndexName,
|
||||
size: limit,
|
||||
body,
|
||||
sort: ["date:desc"],
|
||||
});
|
||||
const {
|
||||
hits: { hits },
|
||||
} = dataResponse.body;
|
||||
const results = hits.map((hit: any) => ({
|
||||
...hit._source,
|
||||
id: hit._id,
|
||||
incident: Array.isArray(hit._source.incident) ? hit._source.incident.join(", ") : hit._source.incident,
|
||||
technology: Array.isArray(hit._source.technology) ? hit._source.technology.join(", ") : hit._source.technology,
|
||||
targeted_group: Array.isArray(hit._source.targeted_group) ? hit._source.targeted_group.join(", ") : hit._source.targeted_group,
|
||||
country: Array.isArray(hit._source.country) ? hit._source.country.join(", ") : hit._source.country,
|
||||
}));
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
const cleanTitle = (title: string) =>
|
||||
title?.replace(/^\[[a-zA-Z]+\] /g, "") ?? "";
|
||||
|
||||
const getVisualizationType = (hit: any) => {
|
||||
const { title, visState } = hit._source.visualization;
|
||||
const rawType = JSON.parse(visState).type;
|
||||
let type = "unknown";
|
||||
if (
|
||||
rawType === "horizontal_bar" &&
|
||||
title.includes("Horizontal Bar Stacked")
|
||||
) {
|
||||
type = "horizontalBarStacked";
|
||||
} else if (rawType === "horizontal_bar" && title.includes("Horizontal Bar")) {
|
||||
type = "horizontalBar";
|
||||
} else if (
|
||||
rawType === "histogram" &&
|
||||
title.includes("Vertical Bar Stacked")
|
||||
) {
|
||||
type = "verticalBarStacked";
|
||||
} else if (rawType === "histogram" && title.includes("Vertical Bar")) {
|
||||
type = "verticalBar";
|
||||
} else if (rawType === "histogram" && title.includes("Line Stacked")) {
|
||||
type = "lineStacked";
|
||||
} else if (rawType === "histogram" && title.includes("Line")) {
|
||||
type = "line";
|
||||
} else if (rawType === "pie") {
|
||||
type = "pieDonut";
|
||||
} else if (rawType === "table") {
|
||||
type = "dataTable";
|
||||
} else if (rawType === "metric") {
|
||||
type = "metric";
|
||||
} else if (rawType === "tagcloud") {
|
||||
type = "tagCloud";
|
||||
}
|
||||
|
||||
return type;
|
||||
};
|
||||
|
||||
export const getTrends = async (limit: number) => {
|
||||
const client = createClient();
|
||||
const query = {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{ match: { type: "visualization" } },
|
||||
{
|
||||
match_bool_prefix: {
|
||||
"visualization.title": "[Trend]",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const rawResponse = await client.search({
|
||||
index: globalIndex,
|
||||
size: limit,
|
||||
body: query,
|
||||
sort: ["updated_at:desc"],
|
||||
});
|
||||
const response = rawResponse.body;
|
||||
const {
|
||||
hits: { hits },
|
||||
} = response;
|
||||
const results = hits.map((hit: any) => ({
|
||||
id: getDocumentID(hit),
|
||||
title: cleanTitle(hit._source.visualization.title),
|
||||
description: hit._source.visualization.description,
|
||||
url: getEmbedURL("global", getDocumentID(hit)),
|
||||
}));
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
export const getTemplates = async (limit: number) => {
|
||||
const client = createClient();
|
||||
|
||||
const query = {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{ match: { type: "visualization" } },
|
||||
{
|
||||
match_bool_prefix: {
|
||||
"visualization.title": "Templated",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const rawResponse = await client.search({
|
||||
index: globalIndex,
|
||||
size: limit,
|
||||
body: query,
|
||||
});
|
||||
|
||||
const response = rawResponse.body;
|
||||
const {
|
||||
hits: { hits },
|
||||
} = response;
|
||||
const results = hits.map((hit: any) => ({
|
||||
id: getDocumentID(hit),
|
||||
title: cleanTitle(hit._source.visualization.title),
|
||||
type: getVisualizationType(hit),
|
||||
}));
|
||||
|
||||
results.sort((a: any, b: any) => a.title.localeCompare(b.title));
|
||||
|
||||
return results;
|
||||
};
|
||||
186
apps/leafcutter/app/_locales/en.json
Normal file
186
apps/leafcutter/app/_locales/en.json
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
{
|
||||
"leafcutterDashboard": "Leafcutter Dashboard",
|
||||
"welcomeToLeafcutter": "Welcome to Leafcutter",
|
||||
"welcomeToLeafcutterDescription": "A Digital Security Threat Analysis Platform from CDR",
|
||||
"signInWith": "Sign In With",
|
||||
"emailMagicLink": "Email a Magic Link",
|
||||
"dontHaveAccount": "Don't have an account?",
|
||||
"requestAccessHere": "Request access here",
|
||||
"goHome": "Go Home",
|
||||
"trendsTitle": "Trends",
|
||||
"trendsSubtitle": "Discover what’s happening globally",
|
||||
"trendsDescription": "Here you will view what digital security threats and trends are facing civil society right now. On a regular basis a CDR team member reviews the aggregated Leafcutter data and creates visualizations. You can also find third party datasets like OONI, and MISP. See all or filter your view using the drop down menu.",
|
||||
"frequentlyAskedQuestionsTitle": "Frequently Asked Questions",
|
||||
"frequentlyAskedQuestionsSubtitle": "Get answers to your questions",
|
||||
"frequentlyAskedQuestionsDescription": "Find out what you want to know about the Leafcutter Project.",
|
||||
"dashboardTitle": "My Dashboard",
|
||||
"dashboardSubtitle": "Welcome",
|
||||
"dashboardDescription": "This is your personal dashboard where all your saved items are stored. All your created visualizations or items you save from the Trends page are stored here.",
|
||||
"searchAndCreateTitle": "Search and Create",
|
||||
"searchAndCreateSubtitle": "Search datasets and create your own visualizations",
|
||||
"searchAndCreateDescription": "Find out how you can use data to create visuals that can be shared externally.",
|
||||
"aboutLeafcutterTitle": "About Leafcutter",
|
||||
"aboutLeafcutterDescription": "A digital threat security analysis dashboard from CDR",
|
||||
"whatIsLeafcutterTitle": "What Is Leafcutter?",
|
||||
"whatIsLeafcutterDescription": "Leafcutter is a secure platform for aggregating, displaying, and sharing data on digital security threats and attacks facing global civil society.",
|
||||
"whatIsItForTitle": "What Is It For?",
|
||||
"whatIsItForDescription": "Leafcutter helps civil society incident responders view and analyze regional and global threat data to get ahead of digital attacks.",
|
||||
"whoCanUseItTitle": "Who Can Use It?",
|
||||
"whoCanUseItDescription": "Leafcutter is available to civil society incident responders and analysts around the world.",
|
||||
"createVisualization": "Create Visualization",
|
||||
"whereDataComesFromTitle": "Where the Data Comes From",
|
||||
"whereDataComesFromDescription": "All data originates from CDR partner communities using the CDR Link helpdesk or from third-party partners gathering data on network filtering, including OONI. All personally identifiable information (PII) is removed before being added to the Leafcutter database, where it is aggregated with data from other CDR Link instances to provide a comprehensive view of digital threats facing civil society in regions around the world.",
|
||||
"projectSupportTitle": "Project Support",
|
||||
"projectSupportDescription": "Leafcutter is a project of Center for Digital Resilience. It is a free and open source tool, built on Open Search and Label Studio. Leafcutter was designed and built by Center for Digital Resilience in collaboration with Julie Kioli (design) and Guardian Project (platform development). Thank you to all who make this project possible.",
|
||||
"interestedInLeafcutterTitle": "Interested in using Leafcutter for your community?",
|
||||
"interestedInLeafcutterDescription": "Leafcutter is part of the CDR Link ecosystem. helping partner communities around the world safely collect data about digital security threats and attacks. Please contact us if you are interested in using Leafcutter.",
|
||||
"dashboardMenuItem": "Dashboard",
|
||||
"dashboardTooltipTitle": "Dashboard",
|
||||
"dashboardTooltipDescription": "Dashboard",
|
||||
"aboutMenuItem": "About",
|
||||
"aboutTooltipTitle": "About",
|
||||
"aboutTooltipDescription": "About",
|
||||
"trendsMenuItem": "Trends",
|
||||
"trendsTooltipTitle": "Trends",
|
||||
"trendsTooltipDescription": "Trends",
|
||||
"searchAndCreateMenuItem": "Search and Create",
|
||||
"searchAndCreateTooltipTitle": "Search and Create",
|
||||
"searchAndCreateTooltipDescription": "Search and Create",
|
||||
"faqMenuItem": "FAQ",
|
||||
"faqTooltipTitle": "FAQ",
|
||||
"faqTooltipDescription": "FAQ",
|
||||
"recentUpdatesTitle": "Recent Updates",
|
||||
"language": "Language",
|
||||
"languageTooltipTitle": "Language select",
|
||||
"languageTooltipDescription": "Select language",
|
||||
"copyright": "Copyright",
|
||||
"projectOf": "A project of",
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"codeOfPractice": "Code of Practice",
|
||||
"contactUs": "Contact Us",
|
||||
"whatIsLeafcutterQuestion": "What is Leafcutter?",
|
||||
"whatIsLeafcutterAnswer": "Leafcutter is a secure platform for aggregating, displaying, and sharing data on digital security threats and attacks facing global civil society.",
|
||||
"whoBuiltLeafcutterQuestion": "Who built Leafcutter?",
|
||||
"whoBuiltLeafcutterAnswer": "Leafcutter was built and is maintained by Center for Digital Resilience [https://digiresilience.org](https://digiresilience.org/).",
|
||||
"whoCanUseLeafcutterQuestion": "Who can use Leafcutter?",
|
||||
"whoCanUseLeafcutterAnswer": "Incident responders, threat analysts, security trainers, and security service providers, in general, can make use of Leafcutter to contextualize threats, draw insights and provide qualitative, data-driven support to the communities they serve.",
|
||||
"whatCanYouDoWithLeafcutterQuestion": "What can you do with Leafcutter?",
|
||||
"whatCanYouDoWithLeafcutterAnswer": "1. Analyze current and previously curated threats and attacks facing the civil society community\n2. Keep track of threats and attacks in other geographies\n3. Make accurate threat predictions with live data and visualizations\n4. Leverage threat data to provide tailored and preventative supports to your communities\n5. Create personalized visualizations to interpret the data in the way that works for you\n6. Share your visualizations with your community or colleagues",
|
||||
"whereIsTheDataComingFromQuestion": "Where is the data coming from?",
|
||||
"whereIsTheDataComingFromAnswer": "Data aggregated into Leafcutter is currently pooled from two sources:\n1. Individual CDR Link instances (CDR's rapid response helpdesk built on Zammad)\n2. Open Observatory of Network Interference (OONI).\nData from OONI is pulled from their public API, and is displayed to give users a sense of global trends in network filtering and interference.\n\nLeafcutter data is also shared with CDR’s MISP instance, to enable sharing of data to other community MISP instances.",
|
||||
"whereIsTheDataStoredQuestion": "Where is the data stored?",
|
||||
"whereIsTheDataStoredAnswer": "Leafcutter data is securely stored in an Amazon Web Services (AWS) database.",
|
||||
"howDoWeKeepTheDataSafeQuestion": "How do we keep the data safe?",
|
||||
"howDoWeKeepTheDataSafeAnswer": "We keep data protected in a number of ways\n1. Tickets and data from CDR Link helpdesks are de-identified and scrubbed of personal/sensitive information before they are aggregated into the Leafcutter database to enable querying via Amazon’s [OpenSearch](https://aws.amazon.com/opensearch-service/the-elk-stack/what-is-opensearch/) service.\n2. Our domain is deployed on a Virtual Private Cloud, eliminating public access to the cluster and data\n3. We employ Amazon's [server side encryption](https://docs.aws.amazon.com/AmazonS3/latest/userguide/serv-side-encryption.html) with [Key Management Service](https://aws.amazon.com/kms/) to encrypt data. Only CDR holds the decryption keys.\n4. Within OpenSearch, we employ security best practices to protect data, including: - Using the latest version of OpenSearch - Employing a least-privilege, restrictive access-control.\n5. For more on CDR’s approach to privacy and security, see our [Privacy Policy](https://digiresilience.org/about/privacy/).",
|
||||
"howLongDoYouKeepTheDataQuestion": "How long do you keep the data?",
|
||||
"howLongDoYouKeepTheDataAnswer": "Ticket data from CDR Link helpdesks are kept only for as long as necessary (detailed [here in our privacy policy](https://digiresilience.org/about/privacy/)). Other non-identifiable data are preserved indefinitely to support historical search and analysis for our community.",
|
||||
"whatOrganizationsAreParticipatingQuestion": "What organizations are participating?",
|
||||
"whatOrganizationsAreParticipatingAnswer": "Leafcutter was built with the civil society security community in mind and features strong participation from organizations and professionals within the [CiviCERT](https://www.civicert.org/) network and the [Threat Intel Coalition](https://www.first.org/global/sigs/tic/), among other rapid response and service provider communities.",
|
||||
"howDidYouGetMyProfileInformationQuestion": "How did you get my profile information?",
|
||||
"howDidYouGetMyProfileInformationAnswer": "We use Google sign-in to authenticate.",
|
||||
"howCanILearnMoreAboutLeafcutterQuestion": "How can I learn more about Leafcutter?",
|
||||
"howCanILearnMoreAboutLeafcutterAnswer": "Contact us at [info@cdr.link](mailto:info@cdr.link) (public PGP key [here](https://digiresilience.org/keys/info.cdr.link-public.txt)).",
|
||||
"of": "of",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"done": "Done",
|
||||
"dashboardNavigationCardTitle": "Dashboard Navigation",
|
||||
"dashboardNavigationCardDescription": "Move between different pages to search data, discover recent trends, and create your own data visualizations.",
|
||||
"recentUpdatesCardTitle": "Recent Updates",
|
||||
"recentUpdatesCardDescription": "Your quick look at recent digital security trends in your community and around the world.",
|
||||
"languageOptionsCardTitle": "Language Options",
|
||||
"languageOptionsCardDescription": "You can change your language preference here and view your dashboard in your preferred language.",
|
||||
"profileSettingsCardTitle": "Profile Settings",
|
||||
"profileSettingsCardDescription": "This is your profile menu, where you can turn on/off your notifications and logout.",
|
||||
"categoriesCardTitle": "Categories",
|
||||
"categoriesCardDescription": "Search global data by choosing one or multiple categories.",
|
||||
"subcategoriesCardTitle": "Subcategories",
|
||||
"subcategoriesCardDescription": "Narrow your search by selecting one or multiple subcategories.",
|
||||
"optionsCardTitle": "Options",
|
||||
"optionsCardDescription": "Choose to Include or Exclude fields from your search.",
|
||||
"incidentTypeCardTitle": "Incident Type",
|
||||
"incidentTypeCardDescription": "What kind of attack or attempted attack was it?",
|
||||
"dateRangeCardTitle": "Date Range",
|
||||
"dateRangeCardDescription": "Choose a beginning and end date for your search or search within a relative timeframe.",
|
||||
"targetedGroupCardTitle": "Targeted Group",
|
||||
"targetedGroupCardDescription": "What individual, organization, or community was the target of the attack?",
|
||||
"impactedTechnologyCardTitle": "Impacted Technology",
|
||||
"impactedTechnologyCardDescription": "What devices, platforms, or services were affected by the incident?",
|
||||
"regionCardTitle": "Region",
|
||||
"regionCardDescription": "Choose geographical regions by country, continent or subregion. When nothing is checked, by default all regions are selected.",
|
||||
"advancedOptionsCardTitle": "+ Advanced Options",
|
||||
"advancedOptionsCardDescription": "Clicking this option will redirect you to the Kibana interface. Kibana is the data visualization platform that powers the Leafcutter dashboard. It provides more advanced search fields and options.",
|
||||
"queryResultsCardTitle": "Your Query & Results",
|
||||
"queryResultsCardDescription": "Your Query displays your chosen search criteria. Blue is what you’ve included and Pink is what you’ve excluded. Your Results display all available search results in a list view based on your search critiera.",
|
||||
"viewResultsCardTitle": "View Results As:",
|
||||
"viewResultsCardDescription": "Below are visualization options for displaying your search results. Highlighted boxes are the available options based on your specific search. Click to expand the visualization, save, or edit.",
|
||||
"technology": "Technology",
|
||||
"actor": "Actor",
|
||||
"geography": "Geography",
|
||||
"channel": "Channel",
|
||||
"status": "Status",
|
||||
"ticketFrequency": "Ticket Frequency",
|
||||
"incidentType": "Incident Type",
|
||||
"type": "Type",
|
||||
"date": "Date",
|
||||
"targetedGroup": "Targeted Group",
|
||||
"group": "Group",
|
||||
"impactedTechnology": "Impacted Technology",
|
||||
"platform": "Platform",
|
||||
"device": "Device",
|
||||
"service": "Service",
|
||||
"maker": "Maker",
|
||||
"region": "Region",
|
||||
"continent": "Continent",
|
||||
"country": "Country",
|
||||
"subregion": "Subregion",
|
||||
"advancedOptions": "Advanced Options",
|
||||
"fullInterfaceWillOpen": "The full OpenSearch Dashboards interface will open in a new window.",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"open": "Open",
|
||||
"include": "Include",
|
||||
"exclude": "Exclude",
|
||||
"startDate": "Start Date",
|
||||
"endDate": "End Date",
|
||||
"relativeDate": "Relative Date",
|
||||
"last7Days": "Last 7 days",
|
||||
"last30Days": "Last 30 days",
|
||||
"last3Months": "Last 3 months",
|
||||
"last6Months": "Last 6 months",
|
||||
"lastYear": "Last year",
|
||||
"last2Years": "Last 2 years",
|
||||
"where": "where",
|
||||
"is": "is",
|
||||
"isNot": "is not",
|
||||
"or": "or",
|
||||
"onOrAfter": "on or after",
|
||||
"onOrBefore": "on or before",
|
||||
"findAllIncidents": "Find all incidents",
|
||||
"title": "Title",
|
||||
"description": "Description",
|
||||
"welcome": "Welcome",
|
||||
"signOut": "Sign Out",
|
||||
"incident": "Incident",
|
||||
"results": "Results",
|
||||
"query": "Query",
|
||||
"viewAs": "View As",
|
||||
"selectVisualization": "Select a Visualization",
|
||||
"selectFieldVisualize": "Select a Field to Visualize",
|
||||
"noSavedVisualizations": "You don’t have any saved visualizations. Go to [Search and Create](/create) or [Trends](/trends) to get started.",
|
||||
"getStartedChecklist": "Get Started Checklist",
|
||||
"searchTitle": "Search",
|
||||
"searchDescription": "This can be trends, new/saved visualizations",
|
||||
"createVisualizationTitle": "Create Visualization",
|
||||
"createVisualizationDescription": "This can be from data in trends",
|
||||
"saveTitle": "Save",
|
||||
"saveDescription": "As a .png, .pdf or .csv",
|
||||
"exportTitle": "Export",
|
||||
"exportDescription": "",
|
||||
"shareTitle": "Share",
|
||||
"shareDescription": "Externally or internally",
|
||||
"savedSearch": "Saved Search",
|
||||
"saveCurrentSearch": "Save Current Search",
|
||||
"clear": "Clear",
|
||||
"delete": "Delete"
|
||||
}
|
||||
44
apps/leafcutter/app/_locales/fr.json
Normal file
44
apps/leafcutter/app/_locales/fr.json
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"aboutLeafcutterTitle": "FRENCH!",
|
||||
"aboutLeafcutterDescription": "FRENCH!",
|
||||
"whatIsLeafcutterTitle": "FRENCH!",
|
||||
"whatIsLeafcutterDescription": "FRENCH!",
|
||||
"whatIsItForTitle": "FRENCH!",
|
||||
"whatIsItForDescription": "FRENCH!",
|
||||
"whoCanUseItTitle": "FRENCH!",
|
||||
"whoCanUseItDescription": "FRENCH!",
|
||||
"createVisualization": "FRENCH!",
|
||||
"whereDataComesFromTitle": "FRENCH!",
|
||||
"whereDataComesFromDescription": "FRENCH!",
|
||||
"projectSupportTitle": "FRENCH!",
|
||||
"projectSupportDescription": "FRENCH!",
|
||||
"interestedInLeafcutterTitle": "FRENCH!",
|
||||
"interestedInLeafcutterDescription": "FRENCH!\nFRENCH!",
|
||||
"myVisualizationsMenuItem": "FRENCH!",
|
||||
"myVisualizationsTooltipTitle": "FRENCH!",
|
||||
"myVisualizationsTooltipDescription": "FRENCH!",
|
||||
"aboutMenuItem": "FRENCH!",
|
||||
"aboutTooltipTitle": "FRENCH!",
|
||||
"aboutTooltipDescription": "FRENCH!",
|
||||
"trendsMenuItem": "FRENCH!",
|
||||
"trendsTooltipTitle": "FRENCH!",
|
||||
"trendsTooltipDescription": "FRENCH!",
|
||||
"searchDataMenuItem": "FRENCH!",
|
||||
"searchDataTooltipTitle": "FRENCH!",
|
||||
"searchDataTooltipDescription": "FRENCH!",
|
||||
"createVisualizationsMenuItem": "FRENCH!",
|
||||
"createVisualizationsTooltipTitle": "FRENCH!",
|
||||
"createVisualizationsTooltipDescription": "FRENCH!",
|
||||
"faqMenuItem": "FRENCH!",
|
||||
"faqTooltipTitle": "FRENCH!",
|
||||
"faqTooltipDescription": "FRENCH!",
|
||||
"recentUpdatesTitle": "FRENCH!",
|
||||
"language": "FRENCH!",
|
||||
"languageTooltipTitle": "FRENCH!",
|
||||
"languageTooltipDescription": "FRENCH!",
|
||||
"copyright": "FRENCH!",
|
||||
"projectOf": "FRENCH!",
|
||||
"privacyPolicy": "FRENCH!",
|
||||
"codeOfPractice": "FRENCH!",
|
||||
"contactUs": "FRENCH!"
|
||||
}
|
||||
5
apps/leafcutter/app/_styles/global.css
Normal file
5
apps/leafcutter/app/_styles/global.css
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
body {
|
||||
overscroll-behavior-x: none;
|
||||
overscroll-behavior-y: none;
|
||||
text-size-adjust: none;
|
||||
}
|
||||
86
apps/leafcutter/app/_styles/theme.ts
Normal file
86
apps/leafcutter/app/_styles/theme.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
export const colors: any = {
|
||||
lightGray: "#ededf0",
|
||||
mediumGray: "#e3e5e5",
|
||||
darkGray: "#33302f",
|
||||
mediumBlue: "#4285f4",
|
||||
green: "#349d7b",
|
||||
lavender: "#a5a6f6",
|
||||
darkLavender: "#5d5fef",
|
||||
pink: "#fcddec",
|
||||
cdrLinkOrange: "#ff7115",
|
||||
coreYellow: "#fac942",
|
||||
helpYellow: "#fff4d5",
|
||||
dwcDarkBlue: "#191847",
|
||||
hazyMint: "#ecf7f8",
|
||||
leafcutterElectricBlue: "#4d6aff",
|
||||
leafcutterLightBlue: "#fafbfd",
|
||||
waterbearElectricPurple: "#332c83",
|
||||
waterbearLightSmokePurple: "#eff3f8",
|
||||
bumpedPurple: "#212058",
|
||||
mutedPurple: "#373669",
|
||||
warningPink: "#ef5da8",
|
||||
lightPink: "#fff0f7",
|
||||
lightGreen: "#f0fff3",
|
||||
lightOrange: "#fff5f0",
|
||||
beige: "#f6f2f1",
|
||||
almostBlack: "#33302f",
|
||||
white: "#ffffff",
|
||||
};
|
||||
|
||||
export const typography: any = {
|
||||
h1: {
|
||||
fontFamily: "Playfair, serif",
|
||||
fontSize: 45,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.1,
|
||||
margin: 0,
|
||||
},
|
||||
h2: {
|
||||
fontFamily: "Poppins, sans-serif",
|
||||
fontSize: 35,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.1,
|
||||
margin: 0,
|
||||
},
|
||||
h3: {
|
||||
fontFamily: "Poppins, sans-serif",
|
||||
fontWeight: 400,
|
||||
fontSize: 27,
|
||||
lineHeight: 1.1,
|
||||
margin: 0,
|
||||
},
|
||||
h4: {
|
||||
fontFamily: "Poppins, sans-serif",
|
||||
fontWeight: 700,
|
||||
fontSize: 18,
|
||||
},
|
||||
h5: {
|
||||
fontFamily: "Roboto, sans-serif",
|
||||
fontWeight: 700,
|
||||
fontSize: 16,
|
||||
lineHeight: "24px",
|
||||
textTransform: "uppercase",
|
||||
textAlign: "center",
|
||||
margin: 1,
|
||||
},
|
||||
h6: {
|
||||
fontFamily: "Roboto, sans-serif",
|
||||
fontWeight: 400,
|
||||
fontSize: 14,
|
||||
textAlign: "center",
|
||||
},
|
||||
p: {
|
||||
fontFamily: "Roboto, sans-serif",
|
||||
fontSize: 17,
|
||||
lineHeight: "26.35px",
|
||||
fontWeight: 400,
|
||||
margin: 0,
|
||||
},
|
||||
small: {
|
||||
fontFamily: "Roboto, sans-serif",
|
||||
fontSize: 13,
|
||||
lineHeight: "18px",
|
||||
fontWeight: 400,
|
||||
margin: 0,
|
||||
},
|
||||
};
|
||||
6
apps/leafcutter/app/api/auth/[...nextauth]/route.ts
Normal file
6
apps/leafcutter/app/api/auth/[...nextauth]/route.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import NextAuth from "next-auth";
|
||||
import { authOptions } from "app/_lib/auth";
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
38
apps/leafcutter/app/api/proxy/[[...path]].ts
Normal file
38
apps/leafcutter/app/api/proxy/[[...path]].ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
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,
|
||||
},
|
||||
};
|
||||
22
apps/leafcutter/app/api/searches/create/route.ts
Normal file
22
apps/leafcutter/app/api/searches/create/route.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "app/_lib/auth";
|
||||
import { getUserMetadata, saveUserMetadata } from "app/_lib/opensearch";
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
const { user: { email } }: any = session;
|
||||
const { name, query } = await req.json();
|
||||
const result = await getUserMetadata(email);
|
||||
const { savedSearches } = result;
|
||||
await saveUserMetadata(email, {
|
||||
savedSearches: [...savedSearches, { name, query }]
|
||||
});
|
||||
const { savedSearches: updatedSavedSearches } = await getUserMetadata(email);
|
||||
|
||||
return NextResponse.json(updatedSavedSearches);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
19
apps/leafcutter/app/api/searches/delete/route.ts
Normal file
19
apps/leafcutter/app/api/searches/delete/route.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "app/_lib/auth";
|
||||
import { getUserMetadata, saveUserMetadata } from "app/_lib/opensearch";
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
const { user: { email } }: any = session;
|
||||
const { name } = await req.json();
|
||||
const { savedSearches } = await getUserMetadata(email);
|
||||
const updatedSavedSearches = savedSearches.filter((search: any) => search.name !== name);
|
||||
const result = await saveUserMetadata(email, { savedSearches: updatedSavedSearches });
|
||||
|
||||
return NextResponse.json({ result });
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
12
apps/leafcutter/app/api/searches/list/route.ts
Normal file
12
apps/leafcutter/app/api/searches/list/route.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "app/_lib/auth";
|
||||
import { getUserMetadata } from "app/_lib/opensearch";
|
||||
|
||||
export const GET = async () => {
|
||||
const session = await getServerSession(authOptions);
|
||||
const { user: { email } }: any = session;
|
||||
const { savedSearches } = await getUserMetadata(email);
|
||||
|
||||
return NextResponse.json(savedSearches);
|
||||
};
|
||||
9
apps/leafcutter/app/api/trends/recent/_route.ts
Normal file
9
apps/leafcutter/app/api/trends/recent/_route.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { getTrends } from "app/_lib/opensearch";
|
||||
|
||||
export const GET = async () => {
|
||||
const results = await getTrends(5);
|
||||
|
||||
NextResponse.json(results);
|
||||
};
|
||||
|
||||
55
apps/leafcutter/app/api/upload/index.ts
Normal file
55
apps/leafcutter/app/api/upload/index.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/* eslint-disable no-restricted-syntax */
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { Client } from "@opensearch-project/opensearch";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import taxonomy from "app/_config/taxonomy.json";
|
||||
import unRegions from "app/_config/unRegions.json";
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
const { tickets } = await req.json();
|
||||
const authorization = req.headers.get("authorization");
|
||||
const baseURL = `https://${process.env.OPENSEARCH_URL}`;
|
||||
const client = new Client({
|
||||
node: baseURL,
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
headers: {
|
||||
authorization
|
||||
}
|
||||
});
|
||||
|
||||
const succeeded = [];
|
||||
const failed = [];
|
||||
|
||||
for await (const ticket of tickets) {
|
||||
const { id } = ticket;
|
||||
try {
|
||||
const country = ticket.country[0] ?? "none";
|
||||
// @ts-expect-error
|
||||
const translatedCountry = taxonomy.country[country]?.display ?? "none";
|
||||
const countryDetails: any = unRegions.find((c) => c.name === translatedCountry);
|
||||
const augmentedTicket = {
|
||||
...ticket,
|
||||
region: countryDetails['sub-region']?.toLowerCase().replace(" ", "-") ?? null,
|
||||
continent: countryDetails.region?.toLowerCase().replace(" ", "-") ?? null,
|
||||
};
|
||||
const out = await client.create({
|
||||
id: uuid(),
|
||||
index: "sample_tagged_tickets",
|
||||
refresh: true,
|
||||
body: augmentedTicket,
|
||||
});
|
||||
console.log(out);
|
||||
succeeded.push(id);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
failed.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
const results = { succeeded, failed };
|
||||
|
||||
return NextResponse.json(results);
|
||||
};
|
||||
|
||||
20
apps/leafcutter/app/api/visualizations/create/route.ts
Normal file
20
apps/leafcutter/app/api/visualizations/create/route.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "app/_lib/auth";
|
||||
import { createUserVisualization } from "app/_lib/opensearch";
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
const { user: { email } }: any = session;
|
||||
const { visualizationID, title, description, query } = await req.json();
|
||||
const id = await createUserVisualization({
|
||||
email,
|
||||
visualizationID,
|
||||
title,
|
||||
description,
|
||||
query
|
||||
});
|
||||
|
||||
return NextResponse.json({ id });
|
||||
};
|
||||
|
||||
15
apps/leafcutter/app/api/visualizations/delete/route.ts
Normal file
15
apps/leafcutter/app/api/visualizations/delete/route.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "app/_lib/auth";
|
||||
import { deleteUserVisualization } from "app/_lib/opensearch";
|
||||
|
||||
export const POST = async (req: NextRequest, res: NextResponse) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
const { user: { email } }: any = session;
|
||||
const { id } = await req.json();
|
||||
await deleteUserVisualization(email as string, id);
|
||||
|
||||
return NextResponse.json({ id });
|
||||
};
|
||||
|
||||
|
||||
12
apps/leafcutter/app/api/visualizations/query/route.ts
Normal file
12
apps/leafcutter/app/api/visualizations/query/route.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { performQuery } from "app/_lib/opensearch";
|
||||
|
||||
export const GET = async (req: NextRequest) => {
|
||||
const searchQuery = req.nextUrl.searchParams.get("searchQuery");
|
||||
const rawQuery = await JSON.parse(decodeURI(searchQuery as string));
|
||||
const results = await performQuery(rawQuery, 1000);
|
||||
|
||||
return NextResponse.json(results);
|
||||
};
|
||||
|
||||
|
||||
21
apps/leafcutter/app/api/visualizations/update/route.ts
Normal file
21
apps/leafcutter/app/api/visualizations/update/route.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "app/_lib/auth";
|
||||
import { updateUserVisualization } from "app/_lib/opensearch";
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
const { user: { email } }: any = session;
|
||||
const { id, title, description, query } = await req.json();
|
||||
await updateUserVisualization({
|
||||
email,
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
query
|
||||
});
|
||||
|
||||
return NextResponse.json({ id });
|
||||
};
|
||||
|
||||
|
||||
32
apps/leafcutter/app/layout.tsx
Normal file
32
apps/leafcutter/app/layout.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import "app/_styles/global.css";
|
||||
import "@fontsource/poppins/400.css";
|
||||
import "@fontsource/poppins/700.css";
|
||||
import "@fontsource/roboto/400.css";
|
||||
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";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Leafcutter",
|
||||
};
|
||||
|
||||
type LayoutProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
// const { publicRuntimeConfig } = getConfig();
|
||||
// LicenseInfo.setLicenseKey(publicRuntimeConfig.muiLicenseKey);
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<MultiProvider>{children}</MultiProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
14
apps/leafcutter/app/page.tsx
Normal file
14
apps/leafcutter/app/page.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "app/_lib/auth";
|
||||
import { getUserVisualizations } from "app/_lib/opensearch";
|
||||
import { Home } from "app/_components/Home";
|
||||
|
||||
export default async function Page() {
|
||||
const session = await getServerSession(authOptions);
|
||||
const {
|
||||
user: { email },
|
||||
}: any = session;
|
||||
const visualizations = await getUserVisualizations(email ?? "none", 20);
|
||||
|
||||
return <Home visualizations={visualizations} />;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue