diff --git a/.gitignore b/.gitignore index 5b9d61b..cc597ab 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ coverage out/ signald-state/* !./signald-state/.gitkeep +baileys-state +signald-state +project.org \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f8492c7..a8a2974 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,7 +17,7 @@ build-all: - turbo build .docker-build: - image: registry.gitlab.com/guardianproject-ops/docker-alpine-git:latest + image: registry.gitlab.com/digiresilience/link/link-stack/buildx:${CI_COMMIT_REF_NAME} services: - docker:dind stage: docker-build @@ -34,7 +34,7 @@ build-all: - docker push ${DOCKER_NS}:${DOCKER_TAG} .docker-release: - image: registry.gitlab.com/guardianproject-ops/docker-alpine-git:latest + image: registry.gitlab.com/digiresilience/link/link-stack/buildx:${CI_COMMIT_REF_NAME} services: - docker:dind stage: docker-release @@ -51,6 +51,17 @@ build-all: - docker tag ${DOCKER_NS}:${DOCKER_TAG} ${DOCKER_NS}:${DOCKER_TAG_NEW} - docker push ${DOCKER_NS}:${DOCKER_TAG_NEW} +buildx-docker-build: + extends: .docker-build + variables: + DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/buildx + DOCKERFILE_PATH: ./docker/buildx/Dockerfile + +buildx-docker-release: + extends: .docker-release + variables: + DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/buildx + link-docker-build: extends: .docker-build variables: @@ -84,17 +95,6 @@ metamigo-docker-release: variables: DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/metamigo -metamigo-frontend-docker-build: - extends: .docker-build - variables: - DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/metamigo-frontend - DOCKERFILE_PATH: ./apps/metamigo-frontend/Dockerfile - -metamigo-frontend-docker-release: - extends: .docker-release - variables: - DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/metamigo-frontend - elasticsearch-docker-build: extends: .docker-build variables: @@ -200,8 +200,40 @@ zammad-docker-build: DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/zammad DOCKERFILE_PATH: ./docker/zammad/Dockerfile DOCKER_CONTEXT: ./docker/zammad + before_script: + - apk --update add nodejs npm + script: + - npm install npm@latest -g + - npm install -g turbo + - npm ci + - turbo build --force --filter zammad-addon-* + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - DOCKER_BUILDKIT=1 docker build --build-arg EMBEDDED=true --pull --no-cache -t ${DOCKER_NS}:${DOCKER_TAG} -f ${DOCKERFILE_PATH} ${DOCKER_CONTEXT} + - docker push ${DOCKER_NS}:${DOCKER_TAG} zammad-docker-release: extends: .docker-release variables: DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/zammad + +zammad-standalone-docker-build: + extends: .docker-build + variables: + DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/zammad-standalone + DOCKERFILE_PATH: ./docker/zammad/Dockerfile + DOCKER_CONTEXT: ./docker/zammad + before_script: + - apk --update add nodejs npm + script: + - npm install npm@latest -g + - npm install -g turbo + - npm ci + - turbo build --force --filter zammad-addon-* + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - DOCKER_BUILDKIT=1 docker build --pull --no-cache -t ${DOCKER_NS}:${DOCKER_TAG} -f ${DOCKERFILE_PATH} ${DOCKER_CONTEXT} + - docker push ${DOCKER_NS}:${DOCKER_TAG} + +zammad-standalone-docker-release: + extends: .docker-release + variables: + DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/zammad-standalone diff --git a/README.md b/README.md index 6b00e89..5b2659e 100644 --- a/README.md +++ b/README.md @@ -14,38 +14,23 @@ Local dev with docker-compose Or for local dev of a single app -* Create `link-stack/.env` from Bitwarden `.env for root of link-stack` +* Create `link-stack/apps/link/.env.local` from Bitwarden `.env.local for link-stack/apps/link` +* Create `link-stack/apps/metamigo-frontend/.metamigo.local.json` from Bitwarden `.metamigo.local.json for link-stack/apps/metamigo/frontend` * Build locally for development: ``` npm install - npm run docker:metamigo:dev:up # start supporting containers for metamigo - npm run build # compile the apps - npm run migrate # this migrates the db - npm run dev:metamigo # this runs metamigo frontend, api, and worker + make dev-metamigo # this starts the containers + npm run migrate # this migrates the db + npm run dev:metamigo # this runs metamigo frontend and api ``` # TODO -Notes from abel regarding metamigo. these are in priority order (high priority first) - -- [ ] Do not upgrade: postgraphile, graphql and other postgres dependencies until postgrahile supports a newer grapqhl version - * ref: https://github.com/graphile/postgraphile/issues/1583 -- [ ] Fix the proxying from metamigo-frontend to metamigo-api, this broke during the next.js `pages/api` -> `app/api/*route.js` change. - * or consider removing the proxy and having the frontend talk directly to the backend, though this may be more work. -- [ ] Upgrade metamigo-frontend react-admin components - * this is the bulk of the real outstanding work, outside of breakages that happend during dep updates between Jun 14 - Aug (of which I'm only aware of the proxying issue, see previous) - * follow react-admin upgrade guide https://marmelab.com/react-admin/Upgrade.html - * in particular: https://marmelab.com/react-admin/Upgrade.html#no-more-prop-injection-in-page-components - * I started this in commit 49650795dff5249c89975d3c0b1cf12836304647 - * so you can follow the same pattern in future commits to fix the signal, whatsapp and twilio pages +- [ ] Delete old JWT config stuff +- [ ] Consolidate config +- [ ] Complete react-admin upgrade.. make all the metamigo-frontend stuff work + * https://marmelab.com/react-admin/Upgrade.html#no-more-prop-injection-in-page-components - [ ] Get metamigo-worker working - * the package.json entry points need to be fixed to be like metamigo-api - * the worker needs a main.ts file like metamigo-api that starts the worker (without the api) `await startWithout(["server"]);` - * while you're at it, I recomnmend moving all source files into a `src` to be consistent with the other metamigo projects - [ ] Migrate off mui/styles * https://mui.com/material-ui/migration/v5-style-changes/ - * the codemods might help us? -- [ ] Delete old JWT config options stuff in `packages/metamigo-config` - * the JWT is no longer used bdad5f551c536d751be87ecb8464d16c82e32699 and 24d52eef3d26ac5ee1294b949490920765fca96f - * so all of the config related to the JWT can be removed: signingkey (and b64 one), encryption key (and b64 one), audience -- [ ] Consolidate config.. this is basically done. The idea is to not need a config js file, everything can be populated from a root level .env file. This is already done, I have been developing like this for awhile, but I notice in your .env file on bitwarden you're still using the config file. \ No newline at end of file + * the codemods might help us? \ No newline at end of file diff --git a/apps/leafcutter/app/(main)/about/page.tsx b/apps/leafcutter/app/(main)/about/page.tsx index 2dc1b39..c92ba88 100644 --- a/apps/leafcutter/app/(main)/about/page.tsx +++ b/apps/leafcutter/app/(main)/about/page.tsx @@ -1,4 +1,4 @@ -import { About } from './_components/About'; +import { About } from "leafcutter-common"; export default function Page() { return ; diff --git a/apps/leafcutter/app/(main)/create/page.tsx b/apps/leafcutter/app/(main)/create/page.tsx index 98a1342..ece4c15 100644 --- a/apps/leafcutter/app/(main)/create/page.tsx +++ b/apps/leafcutter/app/(main)/create/page.tsx @@ -1,8 +1,10 @@ import { getTemplates } from "app/_lib/opensearch"; -import { Create } from "./_components/Create"; +import { Create } from "leafcutter-common"; export default async function Page() { const templates = await getTemplates(100); return ; } + +export const dynamic = "force-dynamic"; diff --git a/apps/leafcutter/app/(main)/faq/page.tsx b/apps/leafcutter/app/(main)/faq/page.tsx index 93c7447..a908dfb 100644 --- a/apps/leafcutter/app/(main)/faq/page.tsx +++ b/apps/leafcutter/app/(main)/faq/page.tsx @@ -1,4 +1,4 @@ -import { FAQ } from "./_components/FAQ"; +import { FAQ } from "leafcutter-common"; export default function Page() { return ; diff --git a/apps/leafcutter/app/(main)/layout.tsx b/apps/leafcutter/app/(main)/layout.tsx index d272093..9d6b1bc 100644 --- a/apps/leafcutter/app/(main)/layout.tsx +++ b/apps/leafcutter/app/(main)/layout.tsx @@ -7,16 +7,12 @@ import "@fontsource/roboto/700.css"; import "@fontsource/playfair-display/900.css"; // import getConfig from "next/config"; // import { LicenseInfo } from "@mui/x-data-grid-pro"; -import { InternalLayout } from "app/_components/InternalLayout"; -import { headers } from 'next/headers' +import { InternalLayout } from "../_components/InternalLayout"; type LayoutProps = { children: ReactNode; }; export default function Layout({ children }: LayoutProps) { - const allHeaders = headers(); - const embedded = Boolean(allHeaders.get('x-leafcutter-embedded')); - - return {children}; + return {children}; } diff --git a/apps/leafcutter/app/page.tsx b/apps/leafcutter/app/(main)/page.tsx similarity index 90% rename from apps/leafcutter/app/page.tsx rename to apps/leafcutter/app/(main)/page.tsx index 95940b0..41ec7df 100644 --- a/apps/leafcutter/app/page.tsx +++ b/apps/leafcutter/app/(main)/page.tsx @@ -1,7 +1,7 @@ import { getServerSession } from "next-auth"; import { authOptions } from "app/_lib/auth"; import { getUserVisualizations } from "app/_lib/opensearch"; -import { Home } from "app/_components/Home"; +import { Home } from "leafcutter-common"; export default async function Page() { const session = await getServerSession(authOptions); diff --git a/apps/leafcutter/app/(main)/preview/[...visualizationID]/page.tsx b/apps/leafcutter/app/(main)/preview/[...visualizationID]/page.tsx index bac4dad..88991c5 100644 --- a/apps/leafcutter/app/(main)/preview/[...visualizationID]/page.tsx +++ b/apps/leafcutter/app/(main)/preview/[...visualizationID]/page.tsx @@ -1,10 +1,10 @@ /* eslint-disable no-underscore-dangle */ // import { Client } from "@opensearch-project/opensearch"; -import { Preview } from "./_components/Preview"; +import { Preview } from "leafcutter-common"; // import { createVisualization } from "lib/opensearch"; export default function Page() { - return ; + return ; } /* diff --git a/apps/leafcutter/app/(main)/setup/_components/Setup.tsx b/apps/leafcutter/app/(main)/setup/_components/Setup.tsx index 3092359..eb98fbe 100644 --- a/apps/leafcutter/app/(main)/setup/_components/Setup.tsx +++ b/apps/leafcutter/app/(main)/setup/_components/Setup.tsx @@ -5,7 +5,7 @@ import { useLayoutEffect } from "react"; import { useRouter } from "next/navigation"; import { Grid, CircularProgress } from "@mui/material"; import Iframe from "react-iframe"; -import { useAppContext } from "app/_components/AppProvider"; +import { useAppContext } from "leafcutter-common/components/AppProvider"; export const Setup: FC = () => { const { @@ -20,6 +20,7 @@ export const Setup: FC = () => { ; } - diff --git a/apps/leafcutter/app/(main)/trends/page.tsx b/apps/leafcutter/app/(main)/trends/page.tsx index 9a33786..221c6e3 100644 --- a/apps/leafcutter/app/(main)/trends/page.tsx +++ b/apps/leafcutter/app/(main)/trends/page.tsx @@ -1,8 +1,10 @@ import { getTrends } from "app/_lib/opensearch"; -import { Trends } from "./_components/Trends"; +import { Trends } from "leafcutter-common"; export default async function Page() { const visualizations = await getTrends(25); return ; } + +export const dynamic = "force-dynamic"; diff --git a/apps/leafcutter/app/(main)/visualizations/[...visualizationID]/page.tsx b/apps/leafcutter/app/(main)/visualizations/[...visualizationID]/page.tsx index aa8d839..0787645 100644 --- a/apps/leafcutter/app/(main)/visualizations/[...visualizationID]/page.tsx +++ b/apps/leafcutter/app/(main)/visualizations/[...visualizationID]/page.tsx @@ -1,6 +1,6 @@ /* eslint-disable no-underscore-dangle */ import { Client } from "@opensearch-project/opensearch"; -import { VisualizationDetail } from "app/_components/VisualizationDetail"; +import { VisualizationDetail } from "leafcutter-common"; const getVisualization = async (visualizationID: string) => { const node = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`; @@ -18,7 +18,7 @@ const getVisualization = async (visualizationID: string) => { const response = rawResponse.body; const hits = response.hits.hits.filter( - (hit: any) => hit._id.split(":")[1] === visualizationID[0] + (hit: any) => hit._id.split(":")[1] === visualizationID[0], ); const hit = hits[0]; const visualization = { diff --git a/apps/leafcutter/app/_components/AccountButton.tsx b/apps/leafcutter/app/_components/AccountButton.tsx index 45a3f4e..2158b82 100644 --- a/apps/leafcutter/app/_components/AccountButton.tsx +++ b/apps/leafcutter/app/_components/AccountButton.tsx @@ -11,7 +11,7 @@ import { bindTrigger, bindMenu, } from "material-ui-popup-state/hooks"; -import { useAppContext } from "./AppProvider"; +import { useAppContext } from "leafcutter-common/components/AppProvider"; export const AccountButton: FC = () => { const t = useTranslate(); diff --git a/apps/leafcutter/app/_components/AppProvider.tsx b/apps/leafcutter/app/_components/AppProvider.tsx index e20d036..5fb065c 100644 --- a/apps/leafcutter/app/_components/AppProvider.tsx +++ b/apps/leafcutter/app/_components/AppProvider.tsx @@ -8,7 +8,7 @@ import { useState, PropsWithChildren, } from "react"; -import { colors, typography } from "app/_styles/theme"; +import { colors, typography } from "leafcutter-common/styles/theme"; const basePath = process.env.GITLAB_CI ? "/link/link-stack/apps/leafcutter" diff --git a/apps/leafcutter/app/_components/HelpButton.tsx b/apps/leafcutter/app/_components/HelpButton.tsx index 6d279dc..48df6e1 100644 --- a/apps/leafcutter/app/_components/HelpButton.tsx +++ b/apps/leafcutter/app/_components/HelpButton.tsx @@ -4,11 +4,11 @@ import { FC, useState } from "react"; import { useRouter, usePathname } from "next/navigation"; import { Button } from "@mui/material"; import { QuestionMark as QuestionMarkIcon } from "@mui/icons-material"; -import { useAppContext } from "./AppProvider"; +import { useAppContext } from "leafcutter-common/components/AppProvider"; export const HelpButton: FC = () => { const router = useRouter(); - const pathname = usePathname(); + const pathname = usePathname() ?? ""; const [helpActive, setHelpActive] = useState(false); const { colors: { leafcutterElectricBlue }, diff --git a/apps/leafcutter/app/_components/InternalLayout.tsx b/apps/leafcutter/app/_components/InternalLayout.tsx index 33166f2..bd859ca 100644 --- a/apps/leafcutter/app/_components/InternalLayout.tsx +++ b/apps/leafcutter/app/_components/InternalLayout.tsx @@ -7,8 +7,8 @@ import CookieConsent from "react-cookie-consent"; import { useCookies } from "react-cookie"; import { TopNav } from "./TopNav"; import { Sidebar } from "./Sidebar"; -import { GettingStartedDialog } from "./GettingStartedDialog"; -import { useAppContext } from "./AppProvider"; +import { GettingStartedDialog } from "leafcutter-common"; +import { useAppContext } from "leafcutter-common/components/AppProvider"; // import { Footer } from "./Footer"; type LayoutProps = PropsWithChildren<{ diff --git a/apps/leafcutter/app/_components/LanguageSelect.tsx b/apps/leafcutter/app/_components/LanguageSelect.tsx index 9887691..43cf363 100644 --- a/apps/leafcutter/app/_components/LanguageSelect.tsx +++ b/apps/leafcutter/app/_components/LanguageSelect.tsx @@ -8,7 +8,7 @@ import { bindTrigger, bindMenu, } from "material-ui-popup-state/hooks"; -import { useAppContext } from "./AppProvider"; +import { useAppContext } from "leafcutter-common/components/AppProvider"; // import { Tooltip } from "./Tooltip"; export const LanguageSelect = () => { diff --git a/apps/leafcutter/app/_components/MultiProvider.tsx b/apps/leafcutter/app/_components/MultiProvider.tsx index c6e6f04..bc441ff 100644 --- a/apps/leafcutter/app/_components/MultiProvider.tsx +++ b/apps/leafcutter/app/_components/MultiProvider.tsx @@ -6,12 +6,12 @@ import { SessionProvider } from "next-auth/react"; import { CssBaseline } from "@mui/material"; import { CookiesProvider } from "react-cookie"; import { I18n } from "react-polyglot"; -import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFns"; +import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFnsV3"; import { LocalizationProvider } from "@mui/x-date-pickers-pro"; -import { AppProvider } from "app/_components/AppProvider"; +import { AppProvider } from "leafcutter-common/components/AppProvider"; import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir"; -import en from "app/_locales/en.json"; -import fr from "app/_locales/fr.json"; +import en from "leafcutter-common/locales/en.json"; +import fr from "leafcutter-common/locales/fr.json"; import "@fontsource/poppins/400.css"; import "@fontsource/poppins/700.css"; import "@fontsource/roboto/400.css"; @@ -21,7 +21,7 @@ import "app/_styles/global.css"; import { LicenseInfo } from "@mui/x-date-pickers-pro"; LicenseInfo.setLicenseKey( - "7c9bf25d9e240f76e77cbf7d2ba58a23Tz02NjU4OCxFPTE3MTU4NjIzMzQ2ODgsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI=" + "7c9bf25d9e240f76e77cbf7d2ba58a23Tz02NjU4OCxFPTE3MTU4NjIzMzQ2ODgsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI=", ); const messages: any = { en, fr }; diff --git a/apps/leafcutter/app/_components/Sidebar.tsx b/apps/leafcutter/app/_components/Sidebar.tsx index b6ddf55..4a8acaf 100644 --- a/apps/leafcutter/app/_components/Sidebar.tsx +++ b/apps/leafcutter/app/_components/Sidebar.tsx @@ -20,8 +20,8 @@ import { import Link from "next/link"; import { usePathname } from "next/navigation"; import { useTranslate } from "react-polyglot"; -import { useAppContext } from "app/_components/AppProvider"; -import { Tooltip } from "app/_components/Tooltip"; +import { useAppContext } from "leafcutter-common/components/AppProvider"; +import { Tooltip } from "leafcutter-common"; // import { ArrowCircleRight as ArrowCircleRightIcon } from "@mui/icons-material"; const MenuItem = ({ @@ -101,8 +101,8 @@ interface SidebarProps { export const Sidebar: FC = ({ open }) => { const t = useTranslate(); - const pathname = usePathname(); - const section = pathname.split("/")[1]; + const pathname = usePathname() ?? ""; + const section = pathname?.split("/")[1]; const { colors: { white }, // leafcutterElectricBlue, leafcutterLightBlue, } = useAppContext(); diff --git a/apps/leafcutter/app/_components/TopNav.tsx b/apps/leafcutter/app/_components/TopNav.tsx index 720d2b1..650f174 100644 --- a/apps/leafcutter/app/_components/TopNav.tsx +++ b/apps/leafcutter/app/_components/TopNav.tsx @@ -6,10 +6,10 @@ import Image from "next/legacy/image"; import { AppBar, Grid, Box } from "@mui/material"; import { useTranslate } from "react-polyglot"; import LeafcutterLogo from "images/leafcutter-logo.png"; -import { AccountButton } from "app/_components/AccountButton"; -import { HelpButton } from "app/_components/HelpButton"; -import { Tooltip } from "app/_components/Tooltip"; -import { useAppContext } from "./AppProvider"; +import { AccountButton } from "./AccountButton"; +import { HelpButton } from "./HelpButton"; +import { Tooltip } from "leafcutter-common"; +import { useAppContext } from "leafcutter-common/components/AppProvider"; // import { LanguageSelect } from "./LanguageSelect"; export const TopNav: FC = () => { @@ -43,50 +43,51 @@ export const TopNav: FC = () => { wrap="nowrap" spacing={4} > - - - - + + + + + + + + Leafcutter + - - - - Leafcutter - - - - - A Project of Center for Digital Resilience - - + + + A Project of Center for Digital Resilience + - + + diff --git a/apps/leafcutter/app/_lib/auth.ts b/apps/leafcutter/app/_lib/auth.ts index a8c9426..24a647a 100644 --- a/apps/leafcutter/app/_lib/auth.ts +++ b/apps/leafcutter/app/_lib/auth.ts @@ -1,8 +1,15 @@ import type { NextAuthOptions } from "next-auth"; import Google from "next-auth/providers/google"; import Apple from "next-auth/providers/apple"; +import Credentials from "next-auth/providers/credentials"; +import { checkAuth } from "./opensearch"; export const authOptions: NextAuthOptions = { + pages: { + signIn: "/login", + error: "/login", + signOut: "/logout", + }, providers: [ Google({ clientId: process.env.GOOGLE_CLIENT_ID ?? "", @@ -12,6 +19,62 @@ export const authOptions: NextAuthOptions = { clientId: process.env.APPLE_CLIENT_ID ?? "", clientSecret: process.env.APPLE_CLIENT_SECRET ?? "", }), + Credentials({ + name: "Link", + credentials: { + authToken: { label: "AuthToken", type: "text", }, + }, + async authorize(credentials, req) { + const { headers } = req; + console.log({ headers }); + const leafcutterUser = headers?.["x-leafcutter-user"]; + const authToken = credentials?.authToken; + + if (!leafcutterUser || leafcutterUser.trim() === "") { + console.log("no leafcutter user"); + return null; + } + + console.log({ authToken }); + return null; + /* + try { + // add role check + await checkAuth(username, password); + const user = { + id: leafcutterUser, + email: leafcutterUser + }; + + return user; + } catch (e) { + console.log({ e }); + } + + return null; + */ + } + }) + ], secret: process.env.NEXTAUTH_SECRET, + /* + callbacks: { + signIn: async ({ user, account, profile }) => { + const roles: any = []; + return roles.includes("admin") || roles.includes("agent"); + }, + session: async ({ session, user, token }) => { + // @ts-ignore + session.user.roles = token.roles; + return session; + }, + jwt: async ({ token, user, account, profile, trigger }) => { + if (user) { + token.roles = []; + } + return token; + } + },*/ }; + diff --git a/apps/leafcutter/app/_lib/opensearch.ts b/apps/leafcutter/app/_lib/opensearch.ts index 46730f6..40996b4 100644 --- a/apps/leafcutter/app/_lib/opensearch.ts +++ b/apps/leafcutter/app/_lib/opensearch.ts @@ -8,9 +8,7 @@ const globalIndex = ".kibana_1"; const dataIndexName = "sample_tagged_tickets"; const userMetadataIndexName = "user_metadata"; -// const baseURL = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`; - -const baseURL = `https://localhost:9200`; +const baseURL = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`; const createClient = () => new Client({ node: baseURL, @@ -23,6 +21,24 @@ const createClient = () => new Client({ }, }); +const createUserClient = (username: string, password: string) => new Client({ + node: baseURL, + auth: { + username, + password, + }, + ssl: { + rejectUnauthorized: false, + }, +}); + +export const checkAuth = async (username: string, password: string) => { + const client = createUserClient(username, password); + const res = await client.ping(); + + return res.statusCode === 200; +}; + const getDocumentID = (doc: any) => doc._id.split(":")[1]; const getEmbedURL = (tenant: string, visualizationID: string) => diff --git a/apps/leafcutter/app/_styles/global.css b/apps/leafcutter/app/_styles/global.css index 4109f02..1546661 100644 --- a/apps/leafcutter/app/_styles/global.css +++ b/apps/leafcutter/app/_styles/global.css @@ -3,3 +3,7 @@ body { overscroll-behavior-y: none; text-size-adjust: none; } + +a { + text-decoration: none; +} diff --git a/apps/leafcutter/app/api/link/auth/route.ts b/apps/leafcutter/app/api/link/auth/route.ts new file mode 100644 index 0000000..37b356e --- /dev/null +++ b/apps/leafcutter/app/api/link/auth/route.ts @@ -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" }); +}; diff --git a/apps/leafcutter/app/api/proxy/[[...path]].ts b/apps/leafcutter/app/api/proxy/[[...path]].ts deleted file mode 100644 index 4956739..0000000 --- a/apps/leafcutter/app/api/proxy/[[...path]].ts +++ /dev/null @@ -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, - }, -}; diff --git a/apps/leafcutter/app/api/trends/recent/_route.ts b/apps/leafcutter/app/api/trends/recent/route.ts similarity index 74% rename from apps/leafcutter/app/api/trends/recent/_route.ts rename to apps/leafcutter/app/api/trends/recent/route.ts index 95d78a8..a635d29 100644 --- a/apps/leafcutter/app/api/trends/recent/_route.ts +++ b/apps/leafcutter/app/api/trends/recent/route.ts @@ -3,7 +3,9 @@ import { getTrends } from "app/_lib/opensearch"; export const GET = async () => { const results = await getTrends(5); + console.log({ results }); NextResponse.json(results); }; +export const dynamic = 'force-dynamic'; diff --git a/apps/leafcutter/app/api/visualizations/list/route.ts b/apps/leafcutter/app/api/visualizations/list/route.ts new file mode 100644 index 0000000..a66f8f4 --- /dev/null +++ b/apps/leafcutter/app/api/visualizations/list/route.ts @@ -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); +}; + diff --git a/apps/leafcutter/app/layout.tsx b/apps/leafcutter/app/layout.tsx index 102c9b7..63ac295 100644 --- a/apps/leafcutter/app/layout.tsx +++ b/apps/leafcutter/app/layout.tsx @@ -8,7 +8,7 @@ import "@fontsource/roboto/700.css"; import "@fontsource/playfair-display/900.css"; // import getConfig from "next/config"; // import { LicenseInfo } from "@mui/x-data-grid-pro"; -import { MultiProvider } from "app/_components/MultiProvider"; +import { MultiProvider } from "./_components/MultiProvider"; export const metadata: Metadata = { title: "Leafcutter", diff --git a/apps/leafcutter/charts/Chart.yaml b/apps/leafcutter/charts/Chart.yaml index f3fa165..8ae83e9 100644 --- a/apps/leafcutter/charts/Chart.yaml +++ b/apps/leafcutter/charts/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: leafcutter description: A Helm chart for Kubernetes type: application -version: 0.1.54 -appVersion: "0.1.54" +version: 0.2.0 +appVersion: "0.2.0" diff --git a/apps/leafcutter/middleware.ts b/apps/leafcutter/middleware.ts index 1840b93..c2e5084 100644 --- a/apps/leafcutter/middleware.ts +++ b/apps/leafcutter/middleware.ts @@ -1,57 +1,6 @@ -import { NextResponse } from "next/server"; -import { withAuth, NextRequestWithAuth } from "next-auth/middleware"; -import getConfig from "next/config"; - -const rewriteURL = (request: NextRequestWithAuth, originBaseURL: string, destinationBaseURL: string, headers: any = {}) => { - if (request.nextUrl.protocol.startsWith('ws')) { - return NextResponse.next(); - } - - if (request.nextUrl.pathname.includes('/_next/static/development/')) { - return NextResponse.next(); - } - - const destinationURL = request.url.replace(originBaseURL, destinationBaseURL); - console.log(`Rewriting ${request.url} to ${destinationURL}`); - - const requestHeaders = new Headers(request.headers); - for (const [key, value] of Object.entries(headers)) { - // @ts-ignore - requestHeaders.set(key, value); - } - requestHeaders.delete('connection'); - - // console.log({ finalHeaders: requestHeaders }); - - return NextResponse.rewrite(new URL(destinationURL), { request: { headers: requestHeaders } }); -}; - -const checkRewrites = async (request: NextRequestWithAuth) => { - console.log({ currentURL: request.nextUrl.href }); - - const leafcutterBaseURL = process.env.LEAFCUTTER_URL ?? "http://localhost:3000"; - const opensearchDashboardsURL = process.env.OPENSEARCH_URL ?? "http://localhost:5602"; - - if (request.nextUrl.pathname.startsWith('/proxy/opensearch')) { - console.log('proxying to zammad'); - const { token } = request.nextauth; - const auth = `${token?.email?.toLowerCase()}:${process.env.OPENSEARCH_USER_PASSWORD}`; - const buff = Buffer.from(auth); - const base64data = buff.toString("base64"); - const headers = { - 'X-Proxy-User': token?.email?.toLowerCase(), - "X-Proxy-Roles": "leafcutter_user", - "Authorization": `Basic ${base64data}` - }; - - console.log({ headers }); - - return rewriteURL(request, `${leafcutterBaseURL}/proxy/opensearch`, opensearchDashboardsURL, headers); - } -}; +import { withAuth } from "next-auth/middleware"; export default withAuth( - checkRewrites, { pages: { signIn: `/login`, @@ -60,25 +9,30 @@ export default withAuth( authorized: ({ token, req }) => { const { url, - headers, } = req; - - // check login page const parsedURL = new URL(url); - if (parsedURL.pathname.startsWith('/login')) { + + console.log({ url }); + console.log({ pathname: parsedURL.pathname }); + console.log({ allowed: parsedURL.pathname.startsWith("/app") }); + const allowed = parsedURL.pathname.startsWith('/login') || parsedURL.pathname.startsWith('/api' || parsedURL.pathname.startsWith("/app")); + if (allowed) { return true; } - // check session auth - const authorizedDomains = ["redaranj.com", "digiresilience.org"]; - const userDomain = token?.email?.toLowerCase().split("@").pop() ?? "unauthorized.net"; - - if (authorizedDomains.includes(userDomain)) { + if (token?.email) { return true; } return false; + }, } } ); + +export const config = { + matcher: [ + '/((?!api|app|bootstrap|3961|ui|translations|internal|login|node_modules|_next/static|_next/image|favicon.ico).*)', + ], +}; diff --git a/apps/leafcutter/next-env.d.ts b/apps/leafcutter/next-env.d.ts index 4f11a03..fd36f94 100644 --- a/apps/leafcutter/next-env.d.ts +++ b/apps/leafcutter/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/leafcutter/next.config.js b/apps/leafcutter/next.config.js index 128e197..066c69a 100644 --- a/apps/leafcutter/next.config.js +++ b/apps/leafcutter/next.config.js @@ -7,9 +7,19 @@ const ContentSecurityPolicy = ` `; module.exports = { - publicRuntimeConfig: { - embedded: true - },/* + transpilePackages: ["leafcutter-common"], + experimental: { + missingSuspenseWithCSRBailout: false, + }, + rewrites: async () => ({ + fallback: [ + { + source: "/:path*", + destination: "/api/proxy/:path*", + }, + ], + }), + /* basePath: "/proxy/leafcutter", assetPrefix: "/proxy/leafcutter", i18n: { @@ -17,25 +27,19 @@ module.exports = { defaultLocale: "en", }, */ - /* rewrites: async () => ({ - fallback: [ - { - source: "/:path*", - destination: "/api/proxy/:path*", - }, - ], - }) */ + + /* async headers() { return [ { source: '/:path*', headers: [ - /* + { key: 'Content-Security-Policy', value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim() }, - */ + { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' @@ -52,4 +56,5 @@ module.exports = { }, ] } + */ }; diff --git a/apps/leafcutter/package.json b/apps/leafcutter/package.json index 14dc267..85df496 100644 --- a/apps/leafcutter/package.json +++ b/apps/leafcutter/package.json @@ -6,7 +6,7 @@ "login": "aws sso login --sso-session cdr", "kubeconfig": "aws eks update-kubeconfig --name cdr-leafcutter-dashboard-cluster --profile cdr-leafcutter-dashboard-production", "fwd:opensearch": "kubectl port-forward opensearch-cluster-master-0 9200:9200 --namespace leafcutter", - "fwd:dashboards": "kubectl port-forward opensearch-dashboards-1-59854cdb9b-vgmtf 5602:5601 --namespace leafcutter", + "fwd:dashboards": "kubectl port-forward opensearch-dashboards-1-59854cdb9b-mx4qq 5602:5601 --namespace leafcutter", "build": "next build", "start": "next start", "export": "next export", @@ -14,51 +14,53 @@ }, "dependencies": { "@emotion/cache": "^11.11.0", - "@emotion/react": "^11.11.1", + "@emotion/react": "^11.11.4", "@emotion/server": "^11.11.0", "@emotion/styled": "^11.11.0", - "@fontsource/playfair-display": "^5.0.5", - "@fontsource/poppins": "^5.0.5", - "@fontsource/roboto": "^5.0.5", + "@fontsource/playfair-display": "^5.0.21", + "@fontsource/poppins": "^5.0.12", + "@fontsource/roboto": "^5.0.12", "@mui/icons-material": "^5", - "@mui/lab": "^5.0.0-alpha.136", + "@mui/lab": "^5.0.0-alpha.167", "@mui/material": "^5", - "@mui/x-data-grid-pro": "^6.10.0", - "@mui/x-date-pickers-pro": "^6.10.0", - "@opensearch-project/opensearch": "^2.3.0", - "date-fns": "^2.30.0", + "@mui/x-data-grid-pro": "^6.19.6", + "@mui/x-date-pickers-pro": "^6.19.6", + "@opensearch-project/opensearch": "^2.5.0", + "cryptr": "^6.3.0", + "date-fns": "^3.3.1", "http-proxy-middleware": "^2.0.6", - "material-ui-popup-state": "^5.0.9", - "next": "13.4.10", - "next-auth": "^4.22.1", - "next-http-proxy-middleware": "^1.2.5", - "nodemailer": "^6.9.3", + "leafcutter-common": "*", + "material-ui-popup-state": "^5.0.10", + "next": "14.1.2", + "next-auth": "^4.24.6", + "next-http-proxy-middleware": "^1.2.6", + "nodemailer": "^6.9.11", "react": "18.2.0", - "react-cookie": "^4.1.1", - "react-cookie-consent": "^8.0.1", + "react-cookie": "^7.1.0", + "react-cookie-consent": "^9.0.0", "react-dom": "18.2.0", "react-iframe": "^1.8.5", - "react-markdown": "^8.0.7", + "react-markdown": "^9.0.1", "react-polyglot": "^0.7.2", - "sharp": "^0.32.3", - "swr": "^2.2.0", - "tss-react": "^4.8.8", - "uuid": "^9.0.0" + "sharp": "^0.33.2", + "swr": "^2.2.5", + "tss-react": "^4.9.4", + "uuid": "^9.0.1" }, "devDependencies": { - "@babel/core": "^7.22.9", - "@types/node": "^20.4.2", - "@types/react": "18.2.15", - "@types/uuid": "^9.0.2", + "@babel/core": "^7.24.0", + "@types/node": "^20.11.24", + "@types/react": "18.2.63", + "@types/uuid": "^9.0.8", "babel-loader": "^9.1.3", - "eslint": "^8.45.0", + "eslint": "^8.57.0", "eslint-config-airbnb": "^19.0.4", - "eslint-config-next": "^13.4.10", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-prettier": "^5.0.0", - "eslint-plugin-react": "^7.32.2", - "typescript": "5.1.6" + "eslint-config-next": "^14.1.2", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react": "^7.34.0", + "typescript": "5.3.3" } } diff --git a/apps/leafcutter/pages/api/proxy/[[...path]].ts b/apps/leafcutter/pages/api/proxy/[[...path]].ts new file mode 100644 index 0000000..913b7cb --- /dev/null +++ b/apps/leafcutter/pages/api/proxy/[[...path]].ts @@ -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, + }, +}; diff --git a/apps/link/app/(login)/login/_components/Login.tsx b/apps/link/app/(login)/login/_components/Login.tsx index bacd448..16331e3 100644 --- a/apps/link/app/(login)/login/_components/Login.tsx +++ b/apps/link/app/(login)/login/_components/Login.tsx @@ -1,9 +1,24 @@ "use client"; -import { FC } from "react"; -import { Box, Grid, Container, IconButton } from "@mui/material"; -import { Apple as AppleIcon, Google as GoogleIcon } from "@mui/icons-material"; +import { FC, useState } from "react"; +import { + Box, + Grid, + Container, + IconButton, + Typography, + TextField, +} from "@mui/material"; +import { + Apple as AppleIcon, + Google as GoogleIcon, + Key as KeyIcon, +} from "@mui/icons-material"; import { signIn } from "next-auth/react"; +import Image from "next/image"; +import LinkLogo from "public/link-logo-small.png"; +import { colors } from "app/_styles/theme"; +import { useSearchParams } from "next/navigation"; type LoginProps = { session: any; @@ -14,62 +29,198 @@ export const Login: FC = ({ session }) => { typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const params = useSearchParams(); + const error = params.get("error"); + const { darkGray, cdrLinkOrange, white } = colors; const buttonStyles = { borderRadius: 500, width: "100%", fontSize: "16px", fontWeight: "bold", + backgroundColor: white, + "&:hover": { + color: white, + backgroundColor: cdrLinkOrange, + }, + }; + const fieldStyles = { + "& label.Mui-focused": { + color: cdrLinkOrange, + }, + "& .MuiInput-underline:after": { + borderBottomColor: cdrLinkOrange, + }, + "& .MuiFilledInput-underline:after": { + borderBottomColor: cdrLinkOrange, + }, + "& .MuiOutlinedInput-root": { + "&.Mui-focused fieldset": { + borderColor: cdrLinkOrange, + }, + }, }; return ( - <> - - - - + + - - - - - - - {!session ? ( - + + - - - signIn("google", { - callbackUrl: `${origin}/setup`, - }) - } - > - - Google - + Link logo + + + + + CDR Link + + + + + + {!session ? ( + + + {error ? ( + + + + {`${error} error`} + + + + ) : null} + + + signIn("google", { + callbackUrl: `${origin}`, + }) + } + > + + Sign in with Google + + + + + signIn("apple", { + callbackUrl: `${window.location.origin}`, + }) + } + > + + Sign in with Apple + + + + + ⸺ or ⸺ + + + + setEmail(e.target.value)} + label="Email" + variant="filled" + size="small" + fullWidth + sx={{ ...fieldStyles, backgroundColor: white }} + /> + + + setPassword(e.target.value)} + label="Password" + variant="filled" + size="small" + fullWidth + sx={{ backgroundColor: white }} + type="password" + /> + + + + signIn("credentials", { + email, + password, + callbackUrl: `${origin}/setup`, + }) + } + > + + Sign in with Zammad credentials + + - {/* - - - signIn("apple", { - callbackUrl: `${window.location.origin}/setup`, - }) - } - > - - - */} - - + ) : null} {session ? ( @@ -79,6 +230,6 @@ export const Login: FC = ({ session }) => { - + ); }; diff --git a/apps/link/app/(main)/_components/ClientOnly.tsx b/apps/link/app/(main)/_components/ClientOnly.tsx new file mode 100644 index 0000000..46e79e8 --- /dev/null +++ b/apps/link/app/(main)/_components/ClientOnly.tsx @@ -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, +}); diff --git a/apps/link/app/(main)/_components/Home.tsx b/apps/link/app/(main)/_components/Home.tsx index 082069e..3196091 100644 --- a/apps/link/app/(main)/_components/Home.tsx +++ b/apps/link/app/(main)/_components/Home.tsx @@ -3,4 +3,4 @@ import { FC } from "react"; import { ZammadWrapper } from "./ZammadWrapper"; -export const Home: FC = () => ; +export const Home: FC = () => ; diff --git a/apps/link/app/(main)/_components/SearchBox.tsx b/apps/link/app/(main)/_components/SearchBox.tsx new file mode 100644 index 0000000..03f0f37 --- /dev/null +++ b/apps/link/app/(main)/_components/SearchBox.tsx @@ -0,0 +1,150 @@ +import { FC, useState, useEffect } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import useSWR from "swr"; +import { Grid, Box, TextField, Autocomplete } from "@mui/material"; +import { searchQuery } from "@/app/_graphql/searchQuery"; +import { colors } from "@/app/_styles/theme"; + +type SearchResultProps = { + props: any; + option: any; +}; + +const SearchInput = (params: any) => ( + +); + +const SearchResult: FC = ({ props, option }) => { + console.log({ option }); + + const { lightGrey, mediumGray, black, white } = colors; + + return ( + + + + + + {option.title} + + + + + + + {option.note} + + + + + + ); +}; + +export const SearchBox: FC = () => { + const [open, setOpen] = useState(false); + const [selectedValue, setSelectedValue] = useState(null); + const [searchTerms, setSearchTerms] = useState(""); + const pathname = usePathname(); + const router = useRouter(); + const { data, error }: any = useSWR({ + document: searchQuery, + variables: { + search: searchTerms ?? "", + limit: 50, + }, + refreshInterval: 10000, + }); + + useEffect(() => { + setOpen(false); + }, [pathname]); + + return ( + setOpen(false)} + inputValue={searchTerms} + onChange={(_event, option, reason) => { + if (!option) return; + const url = `/tickets/${option.internalId}`; + setSelectedValue(""); + router.push(url); + }} + onInputChange={(_event, value) => { + setSearchTerms(value); + }} + open={open} + onOpen={() => setOpen(true)} + noOptionsText="No results" + options={data?.search ?? []} + getOptionLabel={(option: any) => { + if (option) { + return option.title; + } else { + return ""; + } + }} + renderOption={(props, option: any) => ( + + )} + sx={{ width: "100%" }} + renderInput={(params: any) => } + /> + ); +}; diff --git a/apps/link/app/(main)/_components/Sidebar.tsx b/apps/link/app/(main)/_components/Sidebar.tsx index b1813da..9576fee 100644 --- a/apps/link/app/(main)/_components/Sidebar.tsx +++ b/apps/link/app/(main)/_components/Sidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { FC, useState } from "react"; +import { FC, useEffect, useState } from "react"; import useSWR from "swr"; import { Box, @@ -17,12 +17,16 @@ import { import { FeaturedPlayList as FeaturedPlayListIcon, Person as PersonIcon, - Analytics as AnalyticsIcon, + Insights as InsightsIcon, Logout as LogoutIcon, Cottage as CottageIcon, Settings as SettingsIcon, ExpandCircleDown as ExpandCircleDownIcon, Dvr as DvrIcon, + Assessment as AssessmentIcon, + LibraryBooks as LibraryBooksIcon, + School as SchoolIcon, + Search as SearchIcon, } from "@mui/icons-material"; import { usePathname } from "next/navigation"; import Link from "next/link"; @@ -30,6 +34,7 @@ import Image from "next/image"; import LinkLogo from "public/link-logo-small.png"; import { useSession, signOut } from "next-auth/react"; import { getTicketOverviewCountsQuery } from "app/_graphql/getTicketOverviewCountsQuery"; +import { SearchBox } from "./SearchBox"; const openWidth = 270; const closedWidth = 100; @@ -162,20 +167,31 @@ export const Sidebar: FC = ({ open, setOpen }) => { const pathname = usePathname(); const { data: session } = useSession(); const username = session?.user?.name || "User"; + // @ts-ignore + const roles = session?.user?.roles || []; const { data: overviewData, error: overviewError }: any = useSWR( { document: getTicketOverviewCountsQuery, }, - { refreshInterval: 10000 } + { refreshInterval: 10000 }, ); - const findOverviewCountByID = (id: number) => - overviewData?.ticketOverviews?.edges?.find((overview: any) => - overview.node.id.endsWith(`/${id}`) + const findOverviewByName = (name: string) => + overviewData?.ticketOverviews?.edges?.find( + (overview: any) => overview.node.name === name, + )?.node?.id; + const findOverviewCountByID = (id: string) => + overviewData?.ticketOverviews?.edges?.find( + (overview: any) => overview.node.id === id, )?.node?.ticketCount ?? 0; - const assignedCount = findOverviewCountByID(1); - const urgentCount = findOverviewCountByID(7); - const pendingCount = findOverviewCountByID(3); - const unassignedCount = findOverviewCountByID(2); + const recentCount = 0; + const assignedID = findOverviewByName("My Assigned Tickets"); + const assignedCount = findOverviewCountByID(assignedID); + const openID = findOverviewByName("Open Tickets"); + const openCount = findOverviewCountByID(openID); + const urgentID = findOverviewByName("Escalated Tickets"); + const urgentCount = findOverviewCountByID(urgentID); + const unassignedID = findOverviewByName("Unassigned & Open Tickets"); + const unassignedCount = findOverviewCountByID(unassignedID); const logout = () => { signOut({ callbackUrl: "/login" }); @@ -221,6 +237,7 @@ export const Sidebar: FC = ({ open, setOpen }) => { direction="column" justifyContent="space-between" wrap="nowrap" + spacing={0} sx={{ backgroundColor: "#25272A", height: "100%", p: 2 }} > @@ -307,10 +324,30 @@ export const Sidebar: FC = ({ open, setOpen }) => { - + + + + = ({ open, setOpen }) => { /> = ({ open, setOpen }) => { > + = ({ open, setOpen }) => { open={open} /> = ({ open, setOpen }) => { + + = ({ open, setOpen }) => { onClick={undefined} > - {/* = ({ open, setOpen }) => { selected={pathname.endsWith("/leafcutter")} open={open} /> - = ({ open, setOpen }) => { selected={pathname.endsWith("/leafcutter/create")} open={open} /> - = ({ open, setOpen }) => { selected={pathname.endsWith("/leafcutter/trends")} open={open} /> -*/} = ({ open, setOpen }) => { = ({ open, setOpen }) => { selected={pathname.endsWith("/profile")} open={open} /> - - - + {roles.includes("admin") && ( + <> - - - - + + + + {false && roles.includes("metamigo") && ( + + )} + {roles.includes("label_studio") && ( + + )} + + + + )} = ({ columns, rows, onRowClick, - height = "calc(100vh - 20px)", + height = "100%", selectedRows, setSelectedRows, }) => { @@ -43,6 +43,13 @@ export const StyledDataGrid: FC = ({ border: 0, width: "100%", height, + ".MuiDataGrid-row": { + cursor: "pointer", + "&:hover": { + backgroundColor: "#1982fc33 !important", + fontWeight: "bold", + }, + }, ".MuiDataGrid-row:nth-of-type(1n)": { backgroundColor: "#f3f3f3", }, @@ -66,12 +73,14 @@ export const StyledDataGrid: FC = ({ rows={rows} columns={columns} density="compact" - hideFooter + pagination + initialState={{ + pagination: { paginationModel: { pageSize: 25 } }, + }} + pageSizeOptions={[5, 10, 25]} + paginationMode="client" sx={{ height }} rowBuffer={30} - checkboxSelection={!!setSelectedRows} - onRowSelectionModelChange={setSelectedRows} - rowSelectionModel={selectedRows} rowHeight={46} scrollbarSize={0} disableVirtualization diff --git a/apps/link/app/(main)/_components/ZammadWrapper.tsx b/apps/link/app/(main)/_components/ZammadWrapper.tsx index f21d4b7..1696040 100644 --- a/apps/link/app/(main)/_components/ZammadWrapper.tsx +++ b/apps/link/app/(main)/_components/ZammadWrapper.tsx @@ -1,9 +1,10 @@ "use client"; -import { FC, useState } from "react"; -import getConfig from "next/config"; +import { FC, useState, useEffect, useRef } from "react"; import { useRouter } from "next/navigation"; import Iframe from "react-iframe"; +import { useSession } from "next-auth/react"; +import { Box, Grid, CircularProgress } from "@mui/material"; type ZammadWrapperProps = { path: string; @@ -15,68 +16,146 @@ export const ZammadWrapper: FC = ({ hideSidebar = true, }) => { const router = useRouter(); + const { data: session } = useSession({ required: true }); + const timeoutRef = useRef(null); + const [hashCheckComplete, setHashCheckComplete] = useState(false); + const [authenticated, setAuthenticated] = useState(false); const [display, setDisplay] = useState("none"); - const url = `/proxy/zammad${path}`; - console.log({ url }); + const url = `/zammad${path}`; + const id = url.replace(/[^a-zA-Z0-9]/g, ""); - return ( - // @ts-ignore -