More app directory refactoring

This commit is contained in:
Darren Clarke 2023-06-28 09:09:45 +00:00 committed by GitHub
parent b312a8c862
commit 8bbeaa25cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 903 additions and 899 deletions

View file

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

View file

@ -3,7 +3,7 @@
import { FC, useState } from "react"; import { FC, useState } from "react";
import { Dialog, Box, Grid, Checkbox, IconButton } from "@mui/material"; import { Dialog, Box, Grid, Checkbox, IconButton } from "@mui/material";
import { Close as CloseIcon } from "@mui/icons-material"; import { Close as CloseIcon } from "@mui/icons-material";
import { useRouter } from "next/router"; import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { useTranslate } from "react-polyglot"; import { useTranslate } from "react-polyglot";
import { useAppContext } from "./AppProvider"; import { useAppContext } from "./AppProvider";
@ -62,9 +62,11 @@ export const GettingStartedDialog: FC = () => {
typography: { h4 }, typography: { h4 },
} = useAppContext(); } = useAppContext();
const t = useTranslate(); const t = useTranslate();
const [completedItems, setCompletedItems] = useState([] as any[]);
const router = useRouter(); const router = useRouter();
const open = router.query.tooltip?.toString() === "checklist"; const [completedItems, setCompletedItems] = useState([] as any[]);
const searchParams = useSearchParams();
const pathname = usePathname();
const open = searchParams.get("tooltip")?.toString() === "checklist";
const toggleCompletedItem = (item: any) => { const toggleCompletedItem = (item: any) => {
if (completedItems.includes(item)) { if (completedItems.includes(item)) {
setCompletedItems(completedItems.filter((i) => i !== item)); setCompletedItems(completedItems.filter((i) => i !== item));
@ -92,7 +94,7 @@ export const GettingStartedDialog: FC = () => {
<Box sx={{ ...h4, mb: 3 }}>{t("getStartedChecklist")}</Box> <Box sx={{ ...h4, mb: 3 }}>{t("getStartedChecklist")}</Box>
</Grid> </Grid>
<Grid item> <Grid item>
<IconButton onClick={() => router.push(router.pathname)}> <IconButton onClick={() => router.push(pathname)}>
<CloseIcon sx={{ color: almostBlack, fontSize: "18px" }} /> <CloseIcon sx={{ color: almostBlack, fontSize: "18px" }} />
</IconButton> </IconButton>
</Grid> </Grid>

View file

@ -1,20 +1,21 @@
"use client"; "use client";
import { FC, useState } from "react"; import { FC, useState } from "react";
import { useRouter } from "next/router"; import { useRouter, usePathname } from "next/navigation";
import { Button } from "@mui/material"; import { Button } from "@mui/material";
import { QuestionMark as QuestionMarkIcon } from "@mui/icons-material"; import { QuestionMark as QuestionMarkIcon } from "@mui/icons-material";
import { useAppContext } from "./AppProvider"; import { useAppContext } from "./AppProvider";
export const HelpButton: FC = () => { export const HelpButton: FC = () => {
const router = useRouter(); const router = useRouter();
const pathname = usePathname();
const [helpActive, setHelpActive] = useState(false); const [helpActive, setHelpActive] = useState(false);
const { const {
colors: { leafcutterElectricBlue }, colors: { leafcutterElectricBlue },
} = useAppContext(); } = useAppContext();
const onClick = () => { const onClick = () => {
if (helpActive) { if (helpActive) {
router.push(router.pathname); router.push(pathname);
} else { } else {
router.push("/?tooltip=welcome"); router.push("/?tooltip=welcome");
} }

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { useEffect, FC } from "react"; import { useEffect, FC } from "react";
import { useRouter } from "next/router"; import { useRouter, usePathname } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import { Grid, Button } from "@mui/material"; import { Grid, Button } from "@mui/material";
@ -14,11 +14,11 @@ import { useAppContext } from "@/app/_components/AppProvider";
type HomeProps = { type HomeProps = {
visualizations: any; visualizations: any;
embedded: boolean;
}; };
export const Home: FC<HomeProps> = ({ visualizations, embedded }) => { export const Home: FC<HomeProps> = ({ visualizations }) => {
const router = useRouter(); const router = useRouter();
const pathname = usePathname();
const cookieName = "homeIntroComplete"; const cookieName = "homeIntroComplete";
const [cookies, setCookie] = useCookies([cookieName]); const [cookies, setCookie] = useCookies([cookieName]);
const t = useTranslate(); const t = useTranslate();
@ -31,7 +31,7 @@ export const Home: FC<HomeProps> = ({ visualizations, embedded }) => {
useEffect(() => { useEffect(() => {
if (homeIntroComplete === 0) { if (homeIntroComplete === 0) {
setCookie(cookieName, `${1}`, { path: "/" }); setCookie(cookieName, `${1}`, { path: "/" });
router.push(`${router.pathname}?tooltip=welcome`); router.push(`${pathname}?tooltip=welcome`);
} }
}, [homeIntroComplete, router, setCookie]); }, [homeIntroComplete, router, setCookie]);

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { useRouter } from "next/router"; import { useRouter } from "next/navigation";
import { IconButton, Menu, MenuItem, Box } from "@mui/material"; import { IconButton, Menu, MenuItem, Box } from "@mui/material";
import { KeyboardArrowDown as KeyboardArrowDownIcon } from "@mui/icons-material"; import { KeyboardArrowDown as KeyboardArrowDownIcon } from "@mui/icons-material";
import { import {
@ -17,6 +17,7 @@ export const LanguageSelect = () => {
} = useAppContext(); } = useAppContext();
const router = useRouter(); const router = useRouter();
const locales: any = { en: "English", fr: "Français" }; const locales: any = { en: "English", fr: "Français" };
const locale = "en";
const popupState = usePopupState({ variant: "popover", popupId: "language" }); const popupState = usePopupState({ variant: "popover", popupId: "language" });
return ( return (
@ -38,7 +39,7 @@ export const LanguageSelect = () => {
}, },
}} }}
> >
{locales[router.locale as any] ?? locales.en} {locales[locale as any] ?? locales.en}
<KeyboardArrowDownIcon /> <KeyboardArrowDownIcon />
</IconButton> </IconButton>
<Menu {...bindMenu(popupState)}> <Menu {...bindMenu(popupState)}>
@ -46,7 +47,7 @@ export const LanguageSelect = () => {
<MenuItem <MenuItem
key={locale} key={locale}
onClick={() => { onClick={() => {
router.push(router.route, router.route, { locale }); // router.push(router.route, router.route, { locale });
popupState.close(); popupState.close();
}} }}
> >

View file

@ -1,3 +1,5 @@
"use client";
/* eslint-disable react/jsx-props-no-spreading */ /* eslint-disable react/jsx-props-no-spreading */
import { FC, PropsWithChildren } from "react"; import { FC, PropsWithChildren } from "react";
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from "next-auth/react";
@ -5,19 +7,17 @@ import { CssBaseline } from "@mui/material";
import { CookiesProvider } from "react-cookie"; import { CookiesProvider } from "react-cookie";
import { I18n } from "react-polyglot"; import { I18n } from "react-polyglot";
import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFns"; import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFns";
("use client");
import { LocalizationProvider } from "@mui/x-date-pickers-pro"; import { LocalizationProvider } from "@mui/x-date-pickers-pro";
import { AppProvider } from "@/app/_components/AppProvider"; import { AppProvider } from "@/app/_components/AppProvider";
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir"; import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
import en from "locales/en.json"; import en from "app/_locales/en.json";
import fr from "locales/fr.json"; import fr from "app/_locales/fr.json";
import "@fontsource/poppins/400.css"; import "@fontsource/poppins/400.css";
import "@fontsource/poppins/700.css"; import "@fontsource/poppins/700.css";
import "@fontsource/roboto/400.css"; import "@fontsource/roboto/400.css";
import "@fontsource/roboto/700.css"; import "@fontsource/roboto/700.css";
import "@fontsource/playfair-display/900.css"; import "@fontsource/playfair-display/900.css";
import "styles/global.css"; import "app/_styles/global.css";
import { LicenseInfo } from "@mui/x-data-grid-pro"; import { LicenseInfo } from "@mui/x-data-grid-pro";
LicenseInfo.setLicenseKey(process.env.MUI_LICENSE_KEY ?? ""); LicenseInfo.setLicenseKey(process.env.MUI_LICENSE_KEY ?? "");

View file

@ -4,7 +4,7 @@ import { FC, useState, useEffect } from "react";
import { Box, Grid } from "@mui/material"; import { Box, Grid } from "@mui/material";
import { useTranslate } from "react-polyglot"; import { useTranslate } from "react-polyglot";
import taxonomy from "app/_config/taxonomy.json"; import taxonomy from "app/_config/taxonomy.json";
import { colors } from "styles/theme"; import { colors } from "@/app/_styles/theme";
import { useAppContext } from "./AppProvider"; import { useAppContext } from "./AppProvider";
export const QueryText: FC = () => { export const QueryText: FC = () => {

View file

@ -18,7 +18,7 @@ import {
Drawer, Drawer,
} from "@mui/material"; } from "@mui/material";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { usePathname } from "next/navigation";
import { useTranslate } from "react-polyglot"; import { useTranslate } from "react-polyglot";
import { useAppContext } from "@/app/_components/AppProvider"; import { useAppContext } from "@/app/_components/AppProvider";
import { Tooltip } from "@/app/_components/Tooltip"; import { Tooltip } from "@/app/_components/Tooltip";
@ -101,8 +101,8 @@ interface SidebarProps {
export const Sidebar: FC<SidebarProps> = ({ open }) => { export const Sidebar: FC<SidebarProps> = ({ open }) => {
const t = useTranslate(); const t = useTranslate();
const router = useRouter(); const pathname = usePathname();
const section = router.pathname.split("/")[1]; const section = pathname.split("/")[1];
const { const {
colors: { white }, // leafcutterElectricBlue, leafcutterLightBlue, colors: { white }, // leafcutterElectricBlue, leafcutterLightBlue,
} = useAppContext(); } = useAppContext();

View file

@ -2,7 +2,7 @@
/* eslint-disable react/require-default-props */ /* eslint-disable react/require-default-props */
import { FC } from "react"; import { FC } from "react";
import { useRouter } from "next/router"; import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { import {
Box, Box,
Grid, Grid,
@ -40,7 +40,9 @@ export const Tooltip: FC<TooltipProps> = ({
colors: { white, leafcutterElectricBlue, almostBlack }, colors: { white, leafcutterElectricBlue, almostBlack },
} = useAppContext(); } = useAppContext();
const router = useRouter(); const router = useRouter();
const activeTooltip = router.query.tooltip?.toString(); const pathname = usePathname();
const searchParams = useSearchParams();
const activeTooltip = searchParams.get('tooltip')?.toString();
const open = activeTooltip === tooltipID; const open = activeTooltip === tooltipID;
const showNavigation = true; const showNavigation = true;
@ -51,7 +53,7 @@ export const Tooltip: FC<TooltipProps> = ({
<Grid container direction="column"> <Grid container direction="column">
<Grid item container direction="row-reverse"> <Grid item container direction="row-reverse">
<Grid item> <Grid item>
<IconButton onClick={() => router.push(router.pathname)}> <IconButton onClick={() => router.push(pathname)}>
<CloseIcon <CloseIcon
sx={{ sx={{
color: leafcutterElectricBlue, color: leafcutterElectricBlue,
@ -125,7 +127,7 @@ export const Tooltip: FC<TooltipProps> = ({
color: leafcutterElectricBlue, color: leafcutterElectricBlue,
textTransform: "none", textTransform: "none",
}} }}
onClick={() => router.push(router.pathname)} onClick={() => router.push(pathname)}
> >
{t("done")} {t("done")}
</Button> </Button>

View file

@ -8,9 +8,12 @@ import { useAppContext } from "./AppProvider";
export const Welcome = () => { export const Welcome = () => {
const t = useTranslate(); const t = useTranslate();
const { data: session } = useSession(); const { data: session } = useSession();
/*
const { const {
user: { name }, user: { name },
} = session as any; } = session as any;
*/
const name = "Test User";
const { const {
colors: { white, leafcutterElectricBlue }, colors: { white, leafcutterElectricBlue },
typography: { h1, h4, p }, typography: { h1, h4, p },

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { Box, Grid, Dialog, Button } from "@mui/material"; import { Box, Grid, Dialog, Button } from "@mui/material";
import { useRouter } from "next/router"; import { useRouter, useSearchParams } from "next/navigation";
// import { useSession } from "next-auth/react"; // import { useSession } from "next-auth/react";
// import { useTranslate } from "react-polyglot"; // import { useTranslate } from "react-polyglot";
import { useAppContext } from "./AppProvider"; import { useAppContext } from "./AppProvider";
@ -9,13 +9,14 @@ import { useAppContext } from "./AppProvider";
export const WelcomeDialog = () => { export const WelcomeDialog = () => {
// const t = useTranslate(); // const t = useTranslate();
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams();
// const { data: session } = useSession(); // const { data: session } = useSession();
// const { user } = session; // const { user } = session;
const { const {
colors: { white, leafcutterElectricBlue }, colors: { white, leafcutterElectricBlue },
typography: { h1, h6, p }, typography: { h1, h6, p },
} = useAppContext(); } = useAppContext();
const activeTooltip = router.query.tooltip?.toString(); const activeTooltip = searchParams.get('tooltip')?.toString();
const open = activeTooltip === "welcome"; const open = activeTooltip === "welcome";
return ( return (

View 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,
};

View file

@ -1,5 +0,0 @@
import createCache from "@emotion/cache";
export default function createEmotionCache() {
return createCache({ key: "css" });
}

View file

@ -8,10 +8,16 @@ const globalIndex = ".kibana_1";
const dataIndexName = "sample_tagged_tickets"; const dataIndexName = "sample_tagged_tickets";
const userMetadataIndexName = "user_metadata"; const userMetadataIndexName = "user_metadata";
const baseURL = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`; // const baseURL = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
const baseURL = `https://localhost:9200`;
const createClient = () => new Client({ const createClient = () => new Client({
node: baseURL, node: baseURL,
auth: {
username: process.env.OPENSEARCH_USERNAME!,
password: process.env.OPENSEARCH_PASSWORD!,
},
ssl: { ssl: {
rejectUnauthorized: false, rejectUnauthorized: false,
}, },
@ -532,6 +538,7 @@ export const getTrends = async (limit: number) => {
export const getTemplates = async (limit: number) => { export const getTemplates = async (limit: number) => {
const client = createClient(); const client = createClient();
const query = { const query = {
query: { query: {
bool: { bool: {
@ -546,11 +553,14 @@ export const getTemplates = async (limit: number) => {
}, },
}, },
}; };
const rawResponse = await client.search({ const rawResponse = await client.search({
index: globalIndex, index: globalIndex,
size: limit, size: limit,
body: query, body: query,
}); });
const response = rawResponse.body; const response = rawResponse.body;
const { const {
hits: { hits }, hits: { hits },

View file

@ -1,4 +0,0 @@
import { GetServerSidePropsContext } from "next";
export const getEmbedded = (context: GetServerSidePropsContext) =>
context.req.headers["x-leafcutter-embedded"] === "true";

View file

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

View file

@ -1,173 +1,5 @@
import { NextPage, GetServerSideProps, GetServerSidePropsContext } from "next"; import { About } from './_components/About';
import { useTranslate } from "react-polyglot";
import Head from "next/head";
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 "@/app/_components/AboutFeature";
import { AboutBox } from "@/app/_components/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";
import { getEmbedded } from "@/app/_lib/utils";
type AboutProps = { export default function Page() {
embedded: boolean; return <About />;
}; }
const About: NextPage<AboutProps> = ({ embedded }) => {
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>
</>
);
};
export default About;
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => ({ props: { embedded: getEmbedded(context) } });

View file

@ -1,19 +1,6 @@
import NextAuth from "next-auth"; import NextAuth from "next-auth";
import Google from "next-auth/providers/google"; import { authOptions } from "@/app/_lib/auth";
import Apple from "next-auth/providers/apple";
const handler = NextAuth({ const handler = NextAuth(authOptions);
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,
});
export { handler as GET, handler as POST }; export { handler as GET, handler as POST };

View file

@ -1,32 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { getUserMetadata, saveUserMetadata } from "@/app/_lib/opensearch";
export const POST = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
if (req.method !== "POST") {
return res.status(500).json({ message: "Only POST requests are allowed" });
}
const { email }: any = session;
const { name, query } = JSON.parse(req.body);
const result = await getUserMetadata(email);
const { savedSearches } = result;
await saveUserMetadata(email, {
savedSearches: [...savedSearches, { name, query }]
});
const { savedSearches: updatedSavedSearches } = await getUserMetadata(email);
return res.json(updatedSavedSearches);
};

View 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);
};

View file

@ -1,31 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { getUserMetadata, saveUserMetadata } from "@/app/_lib/opensearch";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
if (req.method !== "POST") {
return res.status(500).json({ message: "Only POST requests are allowed" });
}
const { email }: any = session;
const { name } = JSON.parse(req.body);
const { savedSearches } = await getUserMetadata(email);
const updatedSavedSearches = savedSearches.filter((search: any) => search.name !== name);
const result = await saveUserMetadata(email, { savedSearches: updatedSavedSearches });
return res.json({ result });
};
export default handler;

View 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 });
};

View file

@ -1,25 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { getUserMetadata } from "@/app/_lib/opensearch";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
if (req.method !== "GET") {
return res.status(500).json({ message: "Only GET requests are allowed" });
}
const { email }: any = session;
const { savedSearches } = await getUserMetadata(email);
return res.json(savedSearches);
};
export default handler;

View 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);
};

View file

@ -1,20 +0,0 @@
import { NextResponse } from "next/server";
// import { getToken } from "next-auth/jwt";
import { getTrends } from "@/app/_lib/opensearch";
export const GET = async () => {
/*
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
*/
const results = await getTrends(5);
NextResponse.json(results);
};

View 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);
};

View file

@ -1,12 +1,13 @@
/* eslint-disable no-restricted-syntax */ /* eslint-disable no-restricted-syntax */
import { NextApiRequest, NextApiResponse } from "next"; import { NextRequest, NextResponse } from "next/server";
import { Client } from "@opensearch-project/opensearch"; import { Client } from "@opensearch-project/opensearch";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import taxonomy from "app/_config/taxonomy.json"; import taxonomy from "app/_config/taxonomy.json";
import unRegions from "app/_config/unRegions.json"; import unRegions from "app/_config/unRegions.json";
const handler = async (req: NextApiRequest, res: NextApiResponse) => { export const POST = async (req: NextRequest) => {
const { headers: { authorization }, body: { tickets } } = req; const { tickets } = await req.json();
const authorization = req.headers.get("authorization");
const baseURL = `https://${process.env.OPENSEARCH_URL}`; const baseURL = `https://${process.env.OPENSEARCH_URL}`;
const client = new Client({ const client = new Client({
node: baseURL, node: baseURL,
@ -48,7 +49,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
} }
const results = { succeeded, failed }; const results = { succeeded, failed };
return res.json(results);
return NextResponse.json(results);
}; };
export default handler;

View file

@ -1,32 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { createUserVisualization } from "@/app/_lib/opensearch";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
if (req.method !== "POST") {
return res.status(500).json({ message: "Only POST requests are allowed" });
}
const { visualizationID, title, description, query } = req.body;
const id = await createUserVisualization({
email: session.email as string,
visualizationID,
title,
description,
query
});
return res.json({ id });
};
export default handler;

View 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 });
};

View file

@ -1,26 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { deleteUserVisualization } from "@/app/_lib/opensearch";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
if (req.method !== "POST") {
return res.status(500).json({ message: "Only POST requests are allowed" });
}
const { id } = req.body;
await deleteUserVisualization(session.email as string, id);
return res.json({ id });
};
export default handler;

View 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 });
};

View file

@ -1,23 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { performQuery } from "@/app/_lib/opensearch";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
const { searchQuery } = req.query;
const rawQuery = await JSON.parse(decodeURI(searchQuery as string));
const results = await performQuery(rawQuery, 1000);
return res.json(results);
};
export default handler;

View 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);
};

View file

@ -1,32 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { updateUserVisualization } from "@/app/_lib/opensearch";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
if (req.method !== "POST") {
return res.status(500).json({ message: "Only POST requests are allowed" });
}
const { id, title, description, query } = req.body;
await updateUserVisualization({
email: session.email as string,
id,
title,
description,
query
});
return res.json({ id });
};
export default handler;

View file

@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/_lib/auth";
import { updateUserVisualization } from "@/app/_lib/opensearch";
const handler = 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 });
};
export default handler;

View file

@ -0,0 +1,66 @@
"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;
embedded: boolean;
};
export const Create: FC<CreateProps> = ({ templates, embedded }) => {
const t = useTranslate();
const {
colors: { cdrLinkOrange },
typography: { h1, h4 },
} = useAppContext();
const router = useRouter();
const pathname = usePathname();
const cookieName = "searchIntroComplete";
const [cookies, setCookie] = useCookies([cookieName]);
const searchIntroComplete = parseInt(cookies[cookieName], 10) || 0;
useEffect(() => {
if (searchIntroComplete === 0) {
setCookie(cookieName, `${1}`, { path: "/" });
router.push(`${pathname}?group=search&tooltip=1&checklist=1`);
}
}, [searchIntroComplete, router, setCookie]);
return (
<>
<PageHeader backgroundColor={cdrLinkOrange}>
<Grid container direction="row" spacing={2} alignItems="center">
{/* <Grid item xs={2} sx={{ textAlign: "center" }}>
<Image src={SearchCreateHeader} width={100} height={100} alt="" />
</Grid> */}
<Grid container direction="column" item xs={10}>
<Grid item>
<Box component="h1" sx={{ ...h1 }}>
{t("searchAndCreateTitle")}
</Box>
</Grid>
<Grid item>
<Box component="h4" sx={{ ...h4, mt: 1, mb: 1 }}>
{t("searchAndCreateSubtitle")}
</Box>
</Grid>
{/* <Grid>
<Box component="p" sx={{ ...p }}>
{t("searchAndCreateDescription")}
</Box>
</Grid> */}
</Grid>
</Grid>
</PageHeader>
<VisualizationBuilder templates={templates} />
</>
);
};

View file

@ -1,76 +1,9 @@
import { FC, useEffect } from "react";
import { GetServerSideProps, GetServerSidePropsContext } from "next";
import { useTranslate } from "react-polyglot";
import { useRouter } from "next/router";
import { Box, Grid } from "@mui/material";
import { useCookies } from "react-cookie";
import { getTemplates } from "@/app/_lib/opensearch"; import { getTemplates } from "@/app/_lib/opensearch";
import { useAppContext } from "@/app/_components/AppProvider"; import { Create } from "./_components/Create";
import { PageHeader } from "@/app/_components/PageHeader";
import { VisualizationBuilder } from "@/app/_components/VisualizationBuilder";
import { getEmbedded } from "@/app/_lib/utils";
type CreateProps = { export default async function Page() {
templates: any;
embedded: boolean;
};
const Create: FC<CreateProps> = ({ templates, embedded }) => {
const t = useTranslate();
const {
colors: { cdrLinkOrange },
typography: { h1, h4 },
} = useAppContext();
const router = useRouter();
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(`${router.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} />
</>
);
};
export default Create;
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => {
const templates = await getTemplates(100); const templates = await getTemplates(100);
console.log({templates});
return { props: { templates, embedded: getEmbedded(context) } }; return <Create templates={templates} embedded={false}/>;
}; };

View 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} />
))}
</>
);
};

View file

@ -1,111 +1,5 @@
import { useTranslate } from "react-polyglot"; import { FAQ } from "./_components/FAQ";
import { NextPage, GetServerSideProps, GetServerSidePropsContext } from "next";
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";
import { getEmbedded } from "@/app/_lib/utils";
type FAQProps = { export default function Page() {
embedded: boolean; return <FAQ />;
}; }
const FAQ: NextPage<FAQProps> = ({ embedded }) => {
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} />
))}
</>
);
};
export default FAQ;
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => ({ props: { embedded: getEmbedded(context) } });

View file

@ -1,16 +1,16 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import { Metadata } from "next"; import { Metadata } from "next";
import "./_styles/global.css"; import "app/_styles/global.css";
import "@fontsource/poppins/400.css"; import "@fontsource/poppins/400.css";
import "@fontsource/poppins/700.css"; import "@fontsource/poppins/700.css";
import "@fontsource/roboto/400.css"; import "@fontsource/roboto/400.css";
import "@fontsource/roboto/700.css"; import "@fontsource/roboto/700.css";
import "@fontsource/playfair-display/900.css"; import "@fontsource/playfair-display/900.css";
import "styles/global.css";
// import getConfig from "next/config"; // import getConfig from "next/config";
// import { LicenseInfo } from "@mui/x-data-grid-pro"; // import { LicenseInfo } from "@mui/x-data-grid-pro";
import { MultiProvider } from "app/_components/MultiProvider"; import { MultiProvider } from "app/_components/MultiProvider";
import { InternalLayout } from "app/_components/InternalLayout"; import { InternalLayout } from "app/_components/InternalLayout";
import { headers } from 'next/headers'
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Leafcutter", title: "Leafcutter",
@ -21,6 +21,8 @@ type LayoutProps = {
}; };
export default function Layout({ children }: LayoutProps) { export default function Layout({ children }: LayoutProps) {
const allHeaders = headers();
const embedded = Boolean(allHeaders.get('x-leafcutter-embedded'));
// const { publicRuntimeConfig } = getConfig(); // const { publicRuntimeConfig } = getConfig();
// LicenseInfo.setLicenseKey(publicRuntimeConfig.muiLicenseKey); // LicenseInfo.setLicenseKey(publicRuntimeConfig.muiLicenseKey);
@ -28,7 +30,7 @@ export default function Layout({ children }: LayoutProps) {
<html lang="en"> <html lang="en">
<body> <body>
<MultiProvider> <MultiProvider>
<InternalLayout>{children}</InternalLayout> <InternalLayout embedded={embedded}>{children}</InternalLayout>
</MultiProvider> </MultiProvider>
</body> </body>
</html> </html>

View 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>
</>
);
};

View file

@ -1,126 +1,16 @@
import Head from "next/head"; import { Metadata } from "next";
import { NextPage } from "next"; import { getServerSession } from "next-auth";
import Link from "next/link"; import { authOptions } from "app/_lib/auth";
import Image from "next/legacy/image"; import { Login } from "./_components/Login";
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, getSession } from "next-auth/react";
import { useAppContext } from "@/app/_components/AppProvider";
type LoginProps = { export const metadata: Metadata = {
session: any; title: "Login",
}; };
const Login: NextPage<LoginProps> = ({ session }) => { export default async function Page() {
const t = useTranslate(); const session = await getServerSession(authOptions);
const {
colors: { leafcutterElectricBlue, lightGray },
typography: { h1, h4 },
} = useAppContext();
const buttonStyles = {
backgroundColor: lightGray,
borderRadius: 500,
width: "100%",
fontSize: "16px",
fontWeight: "bold",
};
return ( return <Login session={session} />;
<>
<Head>
<title>Leafcutter: Login</title>
</Head>
<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>
</>
);
};
export default Login;
export async function getServerSideProps(context: any) {
const session = (await getSession(context)) ?? null;
return {
props: { session },
};
} }

View file

@ -1,6 +1,5 @@
import { getSession } from "next-auth/react"; import { getSession } from "next-auth/react";
import { getUserVisualizations } from "@/app/_lib/opensearch"; import { getUserVisualizations } from "@/app/_lib/opensearch";
import { getEmbedded } from "@/app/_lib/utils";
import { Home } from "@/app/_components/Home"; import { Home } from "@/app/_components/Home";
export default async function Page() { export default async function Page() {
@ -10,7 +9,6 @@ export default async function Page() {
session?.user?.email ?? "none", session?.user?.email ?? "none",
20 20
); );
const embedded = false; // getEmbedded(context);
return <Home visualizations={visualizations} embedded={embedded} />; return <Home visualizations={visualizations} />;
} }

View file

@ -1,6 +1,6 @@
import { useLayoutEffect } from "react"; import { useLayoutEffect } from "react";
import { NextPage } from "next"; import { NextPage } from "next";
import { useRouter } from "next/router"; import { useRouter } from "next/navigation";
import { Grid, CircularProgress } from "@mui/material"; import { Grid, CircularProgress } from "@mui/material";
import Iframe from "react-iframe"; import Iframe from "react-iframe";
import { useAppContext } from "@/app/_components/AppProvider"; import { useAppContext } from "@/app/_components/AppProvider";

View 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>
</>
);
};

View file

@ -1,84 +1,8 @@
import { NextPage, GetServerSideProps, GetServerSidePropsContext } from "next";
import Head from "next/head";
import { Grid, Box } from "@mui/material";
import { useTranslate } from "react-polyglot";
import { getTrends } from "@/app/_lib/opensearch"; import { getTrends } from "@/app/_lib/opensearch";
import { PageHeader } from "@/app/_components/PageHeader"; import { Trends } from "./_components/Trends";
import { VisualizationCard } from "@/app/_components/VisualizationCard";
import { useAppContext } from "@/app/_components/AppProvider";
import { getEmbedded } from "@/app/_lib/utils";
type TrendsProps = { export default async function Page() {
visualizations: any;
embedded: boolean;
};
const Trends: NextPage<TrendsProps> = ({ visualizations, embedded }) => {
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>
</>
);
};
export default Trends;
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => {
const visualizations = await getTrends(25); const visualizations = await getTrends(25);
return { props: { visualizations, embedded: getEmbedded(context) } }; return <Trends visualizations={visualizations} />;
}; }

View file

@ -1,26 +1,8 @@
/* eslint-disable no-underscore-dangle */ /* eslint-disable no-underscore-dangle */
import { NextPage, GetServerSideProps, GetServerSidePropsContext } from "next";
import { Client } from "@opensearch-project/opensearch"; import { Client } from "@opensearch-project/opensearch";
import { VisualizationDetail } from "@/app/_components/VisualizationDetail"; import { VisualizationDetail } from "@/app/_components/VisualizationDetail";
import { getEmbedded } from "@/app/_lib/utils";
type VisualizationProps = {
visualization: any;
embedded: boolean;
};
const Visualization: NextPage<VisualizationProps> = ({
visualization,
embedded,
}) => <VisualizationDetail {...visualization} />;
export default Visualization;
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => {
const { visualizationID } = context.query;
const getVisualization = async (visualizationID: string) => {
const node = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`; const node = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
const client = new Client({ const client = new Client({
node, node,
@ -36,7 +18,6 @@ export const getServerSideProps: GetServerSideProps = async (
const response = rawResponse.body; const response = rawResponse.body;
const hits = response.hits.hits.filter( const hits = response.hits.hits.filter(
// @ts-expect-error
(hit: any) => hit._id.split(":")[1] === visualizationID[0] (hit: any) => hit._id.split(":")[1] === visualizationID[0]
); );
const hit = hits[0]; const hit = hits[0];
@ -49,5 +30,19 @@ export const getServerSideProps: GetServerSideProps = async (
}?embed=true`, }?embed=true`,
}; };
return { props: { visualization, embedded: getEmbedded(context) } }; 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}/>;
}

View file

@ -1,8 +1,57 @@
import { withAuth } from "next-auth/middleware"; import { NextResponse } from "next/server";
import { withAuth, NextRequestWithAuth } from "next-auth/middleware";
import getConfig from "next/config"; import getConfig from "next/config";
const rewriteURL = (request: NextRequestWithAuth, originBaseURL: string, destinationBaseURL: string, headers: any = {}) => {
if (request.nextUrl.protocol.startsWith('ws')) {
return NextResponse.next();
}
if (request.nextUrl.pathname.includes('/_next/static/development/')) {
return NextResponse.next();
}
const destinationURL = request.url.replace(originBaseURL, destinationBaseURL);
console.log(`Rewriting ${request.url} to ${destinationURL}`);
const requestHeaders = new Headers(request.headers);
for (const [key, value] of Object.entries(headers)) {
// @ts-ignore
requestHeaders.set(key, value);
}
requestHeaders.delete('connection');
// console.log({ finalHeaders: requestHeaders });
return NextResponse.rewrite(new URL(destinationURL), { request: { headers: requestHeaders } });
};
const checkRewrites = async (request: NextRequestWithAuth) => {
console.log({ currentURL: request.nextUrl.href });
const leafcutterBaseURL = process.env.LEAFCUTTER_URL ?? "http://localhost:3000";
const opensearchDashboardsURL = process.env.OPENSEARCH_URL ?? "http://localhost:5602";
if (request.nextUrl.pathname.startsWith('/proxy/opensearch')) {
console.log('proxying to zammad');
const { token } = request.nextauth;
const auth = `${token?.email?.toLowerCase()}:${process.env.OPENSEARCH_USER_PASSWORD}`;
const buff = Buffer.from(auth);
const base64data = buff.toString("base64");
const headers = {
'X-Proxy-User': token?.email?.toLowerCase(),
"X-Proxy-Roles": "leafcutter_user",
"Authorization": `Basic ${base64data}`
};
console.log({ headers });
return rewriteURL(request, `${leafcutterBaseURL}/proxy/opensearch`, opensearchDashboardsURL, headers);
}
};
export default withAuth( export default withAuth(
() => { }, checkRewrites,
{ {
pages: { pages: {
signIn: `/login`, signIn: `/login`,
@ -14,16 +63,6 @@ export default withAuth(
headers, headers,
} = req; } = req;
return true;
/*
const {
publicRuntimeConfig: { embedded },
} = getConfig();
if (embedded) {
return true;
}
// check login page // check login page
const parsedURL = new URL(url); const parsedURL = new URL(url);
if (parsedURL.pathname.startsWith('/login')) { if (parsedURL.pathname.startsWith('/login')) {
@ -39,7 +78,6 @@ export default withAuth(
} }
return false; return false;
*/
}, },
} }
} }

View file

@ -1,14 +1,22 @@
const ContentSecurityPolicy = `
default-src 'self';
script-src 'self';
child-src example.com;
style-src 'self' example.com;
font-src 'self';
`;
module.exports = { module.exports = {
publicRuntimeConfig: { publicRuntimeConfig: {
embedded: true embedded: true
}, },/*
basePath: "/proxy/leafcutter", basePath: "/proxy/leafcutter",
assetPrefix: "/proxy/leafcutter", assetPrefix: "/proxy/leafcutter",
i18n: { i18n: {
locales: ["en", "fr"], locales: ["en", "fr"],
defaultLocale: "en", defaultLocale: "en",
}, },
*/
/* rewrites: async () => ({ /* rewrites: async () => ({
fallback: [ fallback: [
{ {
@ -17,4 +25,31 @@ module.exports = {
}, },
], ],
}) */ }) */
async headers() {
return [
{
source: '/:path*',
headers: [
/*
{
key: 'Content-Security-Policy',
value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
},
*/
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
}
],
},
]
}
}; };

View file

@ -3,7 +3,7 @@
"version": "0.2.0", "version": "0.2.0",
"scripts": { "scripts": {
"dev": "next dev -p 3001", "dev": "next dev -p 3001",
"login": "aws sso login --profile cdr-leafcutter-dashboard-production", "login": "aws sso login --sso-session cdr",
"kubeconfig": "aws eks update-kubeconfig --name cdr-leafcutter-dashboard-cluster --profile cdr-leafcutter-dashboard-production", "kubeconfig": "aws eks update-kubeconfig --name cdr-leafcutter-dashboard-cluster --profile cdr-leafcutter-dashboard-production",
"fwd:opensearch": "kubectl port-forward opensearch-cluster-master-0 9200:9200 --namespace leafcutter", "fwd:opensearch": "kubectl port-forward opensearch-cluster-master-0 9200:9200 --namespace leafcutter",
"fwd:dashboards": "kubectl port-forward opensearch-dashboards-1-59854cdb9b-vgmtf 5602:5601 --namespace leafcutter", "fwd:dashboards": "kubectl port-forward opensearch-dashboards-1-59854cdb9b-vgmtf 5602:5601 --namespace leafcutter",

View file

@ -1,7 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@ -16,9 +20,24 @@
"incremental": true, "incremental": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./*", "../../node_modules/*"] "@/*": [
} "./*",
"../../node_modules/*"
]
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "plugins": [
"exclude": ["node_modules"] {
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
} }