Merge branch 'flatten' into 'develop'

Flatten

See merge request digiresilience/link/link-stack!6
This commit is contained in:
Darren Clarke 2023-08-25 07:11:33 +00:00
commit a9fb4317dc
264 changed files with 9983 additions and 2280 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
/* eslint-disable no-underscore-dangle */ /* eslint-disable no-underscore-dangle */
// import { Client } from "@opensearch-project/opensearch"; // import { Client } from "@opensearch-project/opensearch";
import { Preview } from "./_components/Preview"; import { Preview } from "leafcutter-common";
// import { createVisualization } from "lib/opensearch"; // import { createVisualization } from "lib/opensearch";
export default function Page() { export default function Page() {

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ import {
useState, useState,
PropsWithChildren, PropsWithChildren,
} from "react"; } from "react";
import { colors, typography } from "app/_styles/theme"; import { colors, typography } from "leafcutter-common/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

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

View file

@ -8,10 +8,10 @@ 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";
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 "./AppProvider";
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir"; import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
import en from "app/_locales/en.json"; import en from "leafcutter-common/locales/en.json";
import fr from "app/_locales/fr.json"; import fr from "leafcutter-common/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";
@ -21,7 +21,7 @@ import "app/_styles/global.css";
import { LicenseInfo } from "@mui/x-date-pickers-pro"; import { LicenseInfo } from "@mui/x-date-pickers-pro";
LicenseInfo.setLicenseKey( LicenseInfo.setLicenseKey(
"7c9bf25d9e240f76e77cbf7d2ba58a23Tz02NjU4OCxFPTE3MTU4NjIzMzQ2ODgsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI=" "7c9bf25d9e240f76e77cbf7d2ba58a23Tz02NjU4OCxFPTE3MTU4NjIzMzQ2ODgsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI=",
); );
const messages: any = { en, fr }; const messages: any = { en, fr };

View file

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

View file

@ -6,9 +6,9 @@ import Image from "next/legacy/image";
import { AppBar, Grid, Box } from "@mui/material"; import { AppBar, Grid, Box } from "@mui/material";
import { useTranslate } from "react-polyglot"; import { useTranslate } from "react-polyglot";
import LeafcutterLogo from "images/leafcutter-logo.png"; import LeafcutterLogo from "images/leafcutter-logo.png";
import { AccountButton } from "app/_components/AccountButton"; import { AccountButton } from "./AccountButton";
import { HelpButton } from "app/_components/HelpButton"; import { HelpButton } from "./HelpButton";
import { Tooltip } from "app/_components/Tooltip"; import { Tooltip } from "leafcutter-common";
import { useAppContext } from "./AppProvider"; import { useAppContext } from "./AppProvider";
// import { LanguageSelect } from "./LanguageSelect"; // import { LanguageSelect } from "./LanguageSelect";
@ -43,12 +43,13 @@ export const TopNav: FC = () => {
wrap="nowrap" wrap="nowrap"
spacing={4} spacing={4}
> >
<Link href="/" passHref>
<Grid <Grid
item item
container container
direction="row" direction="row"
justifyContent="flex-start" justifyContent="flex-start"
alignItems="center"
alignContent="center"
spacing={1} spacing={1}
wrap="nowrap" wrap="nowrap"
sx={{ cursor: "pointer" }} sx={{ cursor: "pointer" }}
@ -57,7 +58,7 @@ export const TopNav: FC = () => {
<Image src={LeafcutterLogo} alt="" width={56} height={52} /> <Image src={LeafcutterLogo} alt="" width={56} height={52} />
</Grid> </Grid>
<Grid item container direction="column" alignContent="flex-start"> <Grid item container direction="column" alignContent="flex-start">
<Grid item> <Grid item sx={{ mt: -1 }}>
<Box <Box
sx={{ sx={{
...h5, ...h5,
@ -86,7 +87,7 @@ export const TopNav: FC = () => {
</Grid> </Grid>
</Grid> </Grid>
</Grid> </Grid>
</Link>
<Grid item> <Grid item>
<HelpButton /> <HelpButton />
</Grid> </Grid>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ import "@fontsource/roboto/700.css";
import "@fontsource/playfair-display/900.css"; import "@fontsource/playfair-display/900.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 "./_components/MultiProvider";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Leafcutter", title: "Leafcutter",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,7 +21,7 @@ type TTSProvider = (voice: any, language: any, prompt: any) => Promise<void>;
const tts = async (providerId: any): Promise<TTSProvider> => { const tts = async (providerId: any): Promise<TTSProvider> => {
const r = await fetch( const r = await fetch(
`/api/v1/voice/twilio/text-to-speech-token/${providerId}` `/api/v1/voice/twilio/text-to-speech-token/${providerId}`,
); );
const { token } = await r.json(); const { token } = await r.json();
const twilioClient = await import("twilio-client"); const twilioClient = await import("twilio-client");
@ -97,7 +97,7 @@ export const PromptInput = (form: any, ...rest: any[]) => (
<TextInput <TextInput
source="promptText" source="promptText"
multiline multiline
options={{ fullWidth: true }} // options={{ fullWidth: true }}
InputProps={{ endAdornment: <TextToSpeechButton form={form} /> }} InputProps={{ endAdornment: <TextToSpeechButton form={form} /> }}
{...rest} {...rest}
/> />
@ -106,7 +106,6 @@ export const PromptInput = (form: any, ...rest: any[]) => (
const validateVoice = (_args: any, values: any) => { const validateVoice = (_args: any, values: any) => {
if (!values.language) return "validation.language"; if (!values.language) return "validation.language";
if (!values.voice) return "validation.voice"; if (!values.voice) return "validation.voice";
// @ts-expect-error
const availableVoices = TwilioLanguages.voices[values.language]; const availableVoices = TwilioLanguages.voices[values.language];
const found = const found =
availableVoices.filter((v: any) => v.id === values.voice).length === 1; availableVoices.filter((v: any) => v.id === values.voice).length === 1;
@ -116,7 +115,6 @@ const validateVoice = (_args: any, values: any) => {
}; };
export const VoiceInput = (form: any, ...rest: any[]) => { export const VoiceInput = (form: any, ...rest: any[]) => {
// @ts-expect-error
const voice = TwilioLanguages.voices[form.formData.language] || []; const voice = TwilioLanguages.voices[form.formData.language] || [];
return ( return (
// @ts-expect-error // @ts-expect-error
@ -140,7 +138,7 @@ const getAvailableNumbers = async (providerId: string) => {
return availableNumbers; return availableNumbers;
} catch (error) { } catch (error) {
console.error( console.error(
`Could not fetch available numbers for provider ${providerId} - ${error}` `Could not fetch available numbers for provider ${providerId} - ${error}`,
); );
return []; return [];
} }
@ -287,8 +285,7 @@ export const AsyncSelectInput = (choiceLoader: () => Promise<any[]>, label, sour
export const VoiceLineSelectInput = AsyncSelectInput(getVoiceLineChoices, "Voice Line", "backendId", "validation.noVoiceLines" ) export const VoiceLineSelectInput = AsyncSelectInput(getVoiceLineChoices, "Voice Line", "backendId", "validation.noVoiceLines" )
*/ */
export const VoiceLineSelectInput = (source: string) => () => export const VoiceLineSelectInput = (source: string) => () => (
(
<ReferenceInput <ReferenceInput
label="Voice Line" label="Voice Line"
source={source} source={source}
@ -299,8 +296,7 @@ export const VoiceLineSelectInput = (source: string) => () =>
</ReferenceInput> </ReferenceInput>
); );
export const VoiceLineField = (source: string) => () => export const VoiceLineField = (source: string) => () => (
(
<ReferenceField label="Voice Line" source={source} reference="voiceLines"> <ReferenceField label="Voice Line" source={source} reference="voiceLines">
<TextField source="number" /> <TextField source="number" />
</ReferenceField> </ReferenceField>

View file

@ -55,8 +55,7 @@ export const BackendTypeInput = (props: any) => (
export const BackendIdInput = (form: any, ...rest: any[]) => { export const BackendIdInput = (form: any, ...rest: any[]) => {
const Component = form.formData.backendType const Component = form.formData.backendType
? // @ts-expect-error ? backendInputComponents[form.formData.backendType]
backendInputComponents[form.formData.backendType]
: false; : false;
return <>{Component && <Component form={form} {...rest} />}</>; return <>{Component && <Component form={form} {...rest} />}</>;
}; };
@ -65,8 +64,7 @@ export const BackendIdField = (form: any, ...rest: any[]) => {
console.log(form); console.log(form);
const Component = form.record.backendType const Component = form.record.backendType
? // @ts-expect-error ? backendFieldComponents[form.record.backendType]
backendFieldComponents[form.record.backendType]
: false; : false;
return <>{Component && <Component form={form} {...rest} />}</>; return <>{Component && <Component form={form} {...rest} />}</>;
}; };

View file

@ -24,7 +24,7 @@ const errorLink = onError(
); );
export const apolloClient = new ApolloClient({ export const apolloClient = new ApolloClient({
link: ApolloLink.from([errorLink, new HttpLink({ uri: "/graphql" })]), link: ApolloLink.from([errorLink, new HttpLink({ uri: "/proxy/metamigo/graphql" })]),
cache: new InMemoryCache(), cache: new InMemoryCache(),
/* /*
defaultOptions: { defaultOptions: {

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