diff --git a/.gitignore b/.gitignore index ba0879e..4011990 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules .env .turbo +*.tsbuildinfo build/** **/dist/** .next/** @@ -15,12 +16,18 @@ npm-debug.log* .idea/ *.szpm .env* -yarn-error.log docker-compose.yml coverage .pgpass **/dist/** -.metamigo.local.json +.bridge.local.json out/ signald-state/* !./signald-state/.gitkeep +baileys-state +signald-state +project.org +**/.openapi-generator/ +apps/bridge-worker/scripts/* +ENVIRONMENT_VARIABLES_MIGRATION.md +local-scripts/* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ae97426..8fcd82b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: node:20-bullseye-slim +image: node:22-bookworm-slim stages: - build @@ -8,36 +8,44 @@ stages: build-all: stage: build variables: - TURBO_TOKEN: $TURBO_TOKEN - TURBO_TEAM: $TURBO_TEAM + TURBO_TOKEN: ${TURBO_TOKEN} + TURBO_TEAM: ${TURBO_TEAM} + ZAMMAD_URL: ${ZAMMAD_URL} + PNPM_HOME: "/pnpm" script: - - npm install -g turbo - - npm ci + - export PATH="$PNPM_HOME:$PATH" + - corepack enable && corepack prepare pnpm@9.15.4 --activate + - pnpm add -g turbo + - pnpm install --frozen-lockfile - turbo build .docker-build: - image: registry.gitlab.com/guardianproject-ops/docker-alpine-git:latest + image: registry.gitlab.com/digiresilience/link/link-stack/buildx:main services: - docker:dind stage: docker-build variables: + DOCKER_HOST: tcp://docker:2375 + DOCKER_TLS_CERTDIR: "" DOCKER_TAG: ${CI_COMMIT_SHORT_SHA} - DOCKER_CONTEXT: . + BUILD_CONTEXT: . only: - main - develop - tags script: - 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_BUILDKIT=1 docker build --pull --no-cache -t ${DOCKER_NS}:${DOCKER_TAG} -f ${DOCKERFILE_PATH} ${BUILD_CONTEXT} - 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:main services: - docker:dind stage: docker-release variables: + DOCKER_HOST: tcp://docker:2375 + DOCKER_TLS_CERTDIR: "" DOCKER_TAG: ${CI_COMMIT_SHORT_SHA} DOCKER_TAG_NEW: ${CI_COMMIT_REF_NAME} only: @@ -50,6 +58,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: @@ -61,49 +80,49 @@ link-docker-release: variables: DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/link -leafcutter-docker-build: +bridge-frontend-docker-build: extends: .docker-build variables: - DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/leafcutter - DOCKERFILE_PATH: ./apps/leafcutter/Dockerfile + DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/bridge-frontend + DOCKERFILE_PATH: ./apps/bridge-frontend/Dockerfile -leafcutter-docker-release: +bridge-frontend-docker-release: extends: .docker-release variables: - DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/leafcutter + DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/bridge-frontend -metamigo-docker-build: +bridge-worker-docker-build: extends: .docker-build variables: - DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/metamigo - DOCKERFILE_PATH: ./apps/metamigo-cli/Dockerfile + DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/bridge-worker + DOCKERFILE_PATH: ./apps/bridge-worker/Dockerfile -metamigo-docker-release: +bridge-worker-docker-release: extends: .docker-release variables: - DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/metamigo + DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/bridge-worker -elasticsearch-docker-build: +bridge-whatsapp-docker-build: extends: .docker-build variables: - DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/elasticsearch - DOCKERFILE_PATH: ./docker/elasticsearch/Dockerfile + DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/bridge-whatsapp + DOCKERFILE_PATH: ./apps/bridge-whatsapp/Dockerfile -elasticsearch-docker-release: +bridge-whatsapp-docker-release: extends: .docker-release variables: - DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/elasticsearch + DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/bridge-whatsapp -label-studio-docker-build: +signal-cli-rest-api-docker-build: extends: .docker-build variables: - DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/label-studio - DOCKERFILE_PATH: ./docker/label-studio/Dockerfile + DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/signal-cli-rest-api + DOCKERFILE_PATH: ./docker/signal-cli-rest-api/Dockerfile -label-studio-docker-release: +signal-cli-rest-api-docker-release: extends: .docker-release variables: - DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/label-studio + DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/signal-cli-rest-api memcached-docker-build: extends: .docker-build @@ -171,25 +190,48 @@ redis-docker-release: variables: DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/redis -signald-docker-build: - extends: .docker-build - variables: - DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/signald - DOCKERFILE_PATH: ./docker/signald/Dockerfile - -signald-docker-release: - extends: .docker-release - variables: - DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/signald - zammad-docker-build: extends: .docker-build variables: DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/zammad DOCKERFILE_PATH: ./docker/zammad/Dockerfile - DOCKER_CONTEXT: ./docker/zammad + BUILD_CONTEXT: ./docker/zammad + PNPM_HOME: "/pnpm" + before_script: + - export PATH="$PNPM_HOME:$PATH" + - corepack enable && corepack prepare pnpm@9.15.4 --activate + script: + - pnpm add -g turbo + - pnpm install --frozen-lockfile + - turbo build --force --filter @link-stack/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} ${BUILD_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 + BUILD_CONTEXT: ./docker/zammad + PNPM_HOME: "/pnpm" + before_script: + - export PATH="$PNPM_HOME:$PATH" + - corepack enable && corepack prepare pnpm@9.15.4 --activate + script: + - pnpm add -g turbo + - pnpm install --frozen-lockfile + - turbo build --force --filter @link-stack/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} ${BUILD_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/.gitpod.dockerfile b/.gitpod.dockerfile deleted file mode 100644 index e2b4a7b..0000000 --- a/.gitpod.dockerfile +++ /dev/null @@ -1,78 +0,0 @@ -FROM gitpod/workspace-full - -# install tools we need -RUN set -ex; \ - pyenv global system; \ - sudo add-apt-repository ppa:ansible/ansible; \ - sudo add-apt-repository ppa:maxmind/ppa; \ - curl -s https://helm.baltorepo.com/organization/signing.asc | sudo apt-key add - ; \ - curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash; \ - echo "deb https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list ; \ - sudo apt-get update; \ - sudo apt-get -y upgrade ; \ - sudo apt-get install -y \ - ansible \ - build-essential \ - httpie \ - fd-find \ - ffmpeg \ - geoipupdate \ - gitlab-runner \ - helm \ - htop \ - iotop \ - iptraf \ - jq \ - kitty-terminfo \ - libolm-dev \ - ncdu \ - postgresql \ - pwgen \ - python3-wheel \ - ripgrep \ - rsync \ - scdaemon \ - socat \ - tmux \ - unrar \ - unzip \ - vifm \ - vim \ - yamllint \ - zsh \ - zsh-syntax-highlighting \ - ; sudo rm -rf /var/lib/apt/lists/* - -RUN set -ex; \ - brew install \ - zoxide \ - fzf; - -# needed for tailscale -RUN sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-nft - -# install npm global packages we need -RUN set -ex; \ - npm install -g \ - standard-version \ - turbo \ - ; - -# make a place for all our warez -RUN sudo mkdir -p /usr/local/bin - -# install AWS' kubectl -# from https://docs.aws.amazon.com/eks/latest/userguide/install-kubectl.html -ARG KUBECTL_URL="https://amazon-eks.s3.us-west-2.amazonaws.com/1.21.2/2021-07-05/bin/linux/amd64/kubectl" -RUN set -ex; \ - curl -o kubectl "${KUBECTL_URL}"; \ - chmod +x kubectl; \ - sudo mv kubectl /usr/local/bin - -# install cloudflared -# from https://github.com/cloudflare/cloudflared/releases -ARG CLOUDFLARED_VERSION="2023.2.1" -RUN set -ex; \ - wget --progress=dot:mega https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64.deb; \ - sudo dpkg -i cloudflared-linux-amd64.deb; \ - cloudflared --version diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index b7144e3..0000000 --- a/.gitpod.yml +++ /dev/null @@ -1,63 +0,0 @@ ---- -# build the docker image for our gitpod from this dockerfile -image: - file: .gitpod.dockerfile -# all init+before are run in prebuilds, and on workspace startup -tasks: - - name: npm install - init: | - npm install -# extra extensions we share -vscode: - extensions: - - redhat.vscode-yaml - - ms-azuretools.vscode-docker - - ms-kubernetes-tools.vscode-kubernetes-tools - - ms-vscode.makefile-tools - - bungcip.better-toml - - sleistner.vscode-fileutils - - esbenp.prettier-vscode - - darkriszty.markdown-table-prettify - - VisualStudioExptTeam.vscodeintellicode - -ports: - - name: Zammad - port: 8001 - onOpen: notify - - - name: Leafcutter Local - port: 3001 - onOpen: notify - - - name: Leafcutter - port: 8004 - onOpen: notify - - - name: Link - port: 8003 - onOpen: notify - - - name: Link Local - port: 3000 - onOpen: notify - - - - name: Metamigo - port: 8002 - onOpen: notify - - - name: Metamigo Local - port: 2999 - onOpen: notify - - - name: Metamigo API - port: 8004 - onOpen: notify - - - name: Zammad Postgres - port: 5432 - onOpen: notify - - - name: Metamigo Postgres - port: 5433 - onOpen: notify \ No newline at end of file diff --git a/.nvmrc b/.nvmrc index 6ed5da9..32cfab6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.2.0 +v22.18.0 diff --git a/Makefile b/Makefile deleted file mode 100644 index 0ec923b..0000000 --- a/Makefile +++ /dev/null @@ -1,83 +0,0 @@ -CURRENT_UID := $(shell id -u):$(shell id -g) -PACKAGE_NAME ?= $(shell jq -r '.name' package.json) -PACKAGE_VERSION?= $(shell jq -r '.version' package.json) -BUILD_DATE ?=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") -DOCKER_ARGS ?= -DOCKER_NS ?= registry.gitlab.com/digiresilience/link/${PACKAGE_NAME} -DOCKER_TAG ?= test -DOCKER_BUILD := docker build ${DOCKER_ARGS} --build-arg BUILD_DATE=${BUILD_DATE} -DOCKER_BUILD_FRESH := ${DOCKER_BUILD} --pull --no-cache -DOCKER_BUILD_ARGS := --build-arg VCS_REF=${CI_COMMIT_SHORT_SHA} -DOCKER_PUSH := docker push -DOCKER_BUILD_TAG := ${DOCKER_NS}:${DOCKER_TAG} - -.PHONY: .npmrc -.EXPORT_ALL_VARIABLES: - -.npmrc: -ifdef CI_JOB_TOKEN - echo '@guardianproject-ops:registry=https://gitlab.com/api/v4/packages/npm/' > .npmrc - echo '@digiresilience:registry=https://gitlab.com/api/v4/packages/npm/' >> .npmrc - echo '//gitlab.com/api/v4/packages/npm/:_authToken=${CI_JOB_TOKEN}' >> .npmrc - echo '//gitlab.com/api/v4/projects/:_authToken=${CI_JOB_TOKEN}' >> .npmrc - echo '//gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}' >> .npmrc -endif - -docker/build: .npmrc - DOCKER_BUILDKIT=1 ${DOCKER_BUILD} ${DOCKER_BUILD_ARGS} -t ${DOCKER_BUILD_TAG} ${PWD} - -docker/build-fresh: .npmrc - DOCKER_BUILDKIT=1 ${DOCKER_BUILD_FRESH} ${DOCKER_BUILD_ARGS} -t ${DOCKER_BUILD_TAG} ${PWD} - -docker/add-tag: - docker pull ${DOCKER_NS}:${DOCKER_TAG} - docker tag ${DOCKER_NS}:${DOCKER_TAG} ${DOCKER_NS}:${DOCKER_TAG_NEW} - docker push ${DOCKER_NS}:${DOCKER_TAG_NEW} - -docker/push: - ${DOCKER_PUSH} ${DOCKER_BUILD_TAG} - -docker/build-push: docker/build docker/push -docker/build-fresh-push: docker/build-fresh docker/push - -# don't use this to generate passwords for production -generate-secrets: - ZAMMAD_DATABASE_PASSWORD=$(shell openssl rand -hex 16) - METAMIGO_DATABASE_ROOT_PASSWORD=$(shell openssl rand -hex 16) - METAMIGO_DATABASE_PASSWORD=$(shell openssl rand -hex 16) - METAMIGO_DATABASE_AUTHENTICATOR_PASSWORD=$(shell openssl rand -hex 16) - NEXTAUTH_AUDIENCE=$(shell openssl rand -hex 16) - NEXTAUTH_SECRET=$(shell openssl rand -hex 16) - -generate-keys: - docker exec -i $(shell docker ps -aqf "name=metamigo-frontend") bash -c "/opt/metamigo/cli gen-jwks" - -setup-signal: - mkdir -p signald - -create-admin-user: - docker exec -i $(shell docker ps -aqf "name=metamigo-postgresql") bash < ./scripts/create-admin-user.sh - - -.env: - @test -f .env || echo "You must create .env please refer to the README" && exit 1 - -start: .env - CURRENT_UID=$(CURRENT_UID) docker-compose up -d - -start-dev: .env - CURRENT_UID=$(CURRENT_UID) docker-compose up --build -d - -restart: .env - CURRENT_UID=$(CURRENT_UID) docker restart $(shell docker ps -a -q) - -stop: - CURRENT_UID=$(CURRENT_UID) docker-compose down - -destroy: - docker-compose down - docker volume prune - - -dev-metamigo: - CURRENT_UID=$(CURRENT_UID) docker compose up -d metamigo-postgresql signald diff --git a/README.md b/README.md index 5b2659e..155729c 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,23 @@ -# Dev Setup +# CDR Link -> NOTE: When using Gitpod/Codespaces, use at least 16GB RAM +CDR Link is a simple & streamlined helpdesk that lets you tag, assign and respond to your tickets. It is developed by the [Center for Digital Resilience](https://digiresilience.org) and powered by [Zammad](https://zammad.org). -Local dev with docker-compose +Key differences between CDR Link and a standard Zammad installation: -* Create `link-stack/.env` from Bitwarden `.env for root of link-stack` -* Run local dev with docker-compose: - ``` - git clone ... - cd link-stack - make start-dev - ``` +- In addition to the full Zammad interface, CDR Link also provides a simplified 'shell' interface that focuses on the most-commonly-used functionality. +- Additional channels to communicate with users, including Signal, Whatsapp & Twilio voice messaging. +- More stringent privacy defaults: ticket data is never sent over email and calls to third-party services are restricted. -Or for local dev of a single app +## Developing -* 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 - make dev-metamigo # this starts the containers - npm run migrate # this migrates the db - npm run dev:metamigo # this runs metamigo frontend and api - ``` +This is a monorepo that contains CDR Link and several supporting applications and libraries. It also includes Dockerfiles to build all of the other containers required for an installation. By tagging our own versions of these dependencies, we can make sure that different versions of the supporting containers all work together and are updated in sync. -# TODO +We use [Turborepo](https://turbo.build) to manage development and building of the packages. To get started: + +- `npm install` in the root directory +- `turbo build` to build all packages + +To run a single package: + +- `turbo dev --filter @link-stack/link` -- [ ] 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 -- [ ] Migrate off mui/styles - * https://mui.com/material-ui/migration/v5-style-changes/ - * the codemods might help us? \ No newline at end of file diff --git a/apps/bridge-frontend/.eslintrc.json b/apps/bridge-frontend/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/apps/bridge-frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/apps/leafcutter/.gitignore b/apps/bridge-frontend/.gitignore similarity index 74% rename from apps/leafcutter/.gitignore rename to apps/bridge-frontend/.gitignore index 30276b2..fd3dbb5 100644 --- a/apps/leafcutter/.gitignore +++ b/apps/bridge-frontend/.gitignore @@ -4,6 +4,7 @@ /node_modules /.pnp .pnp.js +.yarn/install-state.gz # testing /coverage @@ -25,16 +26,11 @@ yarn-debug.log* yarn-error.log* # local env files -.env.local -.env.development.local -.env.test.local -.env.production.local +.env*.local # vercel .vercel -/storybook-static - -*.tgz - -.vscode +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/bridge-frontend/Dockerfile b/apps/bridge-frontend/Dockerfile new file mode 100644 index 0000000..75a19ce --- /dev/null +++ b/apps/bridge-frontend/Dockerfile @@ -0,0 +1,54 @@ +FROM node:22-bookworm-slim AS base + +FROM base AS builder +ARG APP_DIR=/opt/bridge-frontend +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN mkdir -p ${APP_DIR}/ +RUN corepack enable && corepack prepare pnpm@9.15.4 --activate +RUN pnpm add -g turbo +WORKDIR ${APP_DIR} +COPY . . +RUN turbo prune --scope=@link-stack/bridge-frontend --scope=@link-stack/bridge-migrations --docker + +FROM base AS installer +ARG APP_DIR=/opt/bridge-frontend +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +WORKDIR ${APP_DIR} +RUN corepack enable && corepack prepare pnpm@9.15.4 --activate +COPY --from=builder ${APP_DIR}/.gitignore .gitignore +COPY --from=builder ${APP_DIR}/out/json/ . +COPY --from=builder ${APP_DIR}/out/pnpm-lock.yaml ./pnpm-lock.yaml +RUN pnpm install --frozen-lockfile + +COPY --from=builder ${APP_DIR}/out/full/ . +RUN pnpm add -g turbo +RUN turbo run build --filter=@link-stack/bridge-frontend --filter=@link-stack/bridge-migrations + +FROM base AS runner +ARG APP_DIR=/opt/bridge-frontend +WORKDIR ${APP_DIR}/ +ARG BUILD_DATE +ARG VERSION +LABEL maintainer="Darren Clarke " +LABEL org.label-schema.build-date=$BUILD_DATE +LABEL org.label-schema.version=$VERSION +ENV APP_DIR ${APP_DIR} +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable && corepack prepare pnpm@9.15.4 --activate +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + apt-get install -y --no-install-recommends \ + dumb-init +RUN mkdir -p ${APP_DIR} +WORKDIR ${APP_DIR} +COPY --from=installer ${APP_DIR} ./ +RUN chown -R node:node ${APP_DIR}/ +WORKDIR ${APP_DIR}/apps/bridge-frontend/ +RUN chmod +x docker-entrypoint.sh +USER node +EXPOSE 3000 +ENV PORT 3000 +ENV NODE_ENV production +ENTRYPOINT ["/opt/bridge-frontend/apps/bridge-frontend/docker-entrypoint.sh"] diff --git a/apps/bridge-frontend/README.md b/apps/bridge-frontend/README.md new file mode 100644 index 0000000..ebec248 --- /dev/null +++ b/apps/bridge-frontend/README.md @@ -0,0 +1,133 @@ +# Bridge Frontend + +Frontend application for managing communication bridges between various messaging platforms and the CDR Link system. + +## Overview + +Bridge Frontend provides a web interface for configuring and managing communication channels including Signal, WhatsApp, Facebook, and Voice integrations. It handles bot registration, webhook configuration, and channel settings. + +## Features + +- **Channel Management**: Configure Signal, WhatsApp, Facebook, and Voice channels +- **Bot Registration**: Register and manage bots for each communication platform +- **Webhook Configuration**: Set up webhooks for message routing +- **Settings Management**: Configure channel-specific settings and behaviors +- **User Authentication**: Secure access with NextAuth.js + +## Development + +### Prerequisites + +- Node.js >= 20 +- npm >= 10 +- PostgreSQL database +- Running bridge-worker service + +### Setup + +```bash +# Install dependencies +npm install + +# Run database migrations +npm run migrate:latest + +# Run development server +npm run dev + +# Build for production +npm run build + +# Start production server +npm run start +``` + +### Environment Variables + +Required environment variables: + +- `DATABASE_URL` - PostgreSQL connection string +- `DATABASE_HOST` - Database host +- `DATABASE_NAME` - Database name +- `DATABASE_USER` - Database username +- `DATABASE_PASSWORD` - Database password +- `NEXTAUTH_URL` - Application URL +- `NEXTAUTH_SECRET` - NextAuth.js secret +- `GOOGLE_CLIENT_ID` - Google OAuth client ID +- `GOOGLE_CLIENT_SECRET` - Google OAuth client secret + +### Available Scripts + +- `npm run dev` - Start development server +- `npm run build` - Build for production +- `npm run start` - Start production server +- `npm run lint` - Run ESLint +- `npm run migrate:latest` - Run all pending migrations +- `npm run migrate:down` - Rollback last migration +- `npm run migrate:up` - Run next migration +- `npm run migrate:make` - Create new migration + +## Architecture + +### Database Schema + +The application manages the following main entities: + +- **Bots**: Communication channel bot configurations +- **Webhooks**: Webhook endpoints for external integrations +- **Settings**: Channel-specific configuration settings +- **Users**: User accounts with role-based permissions + +### API Routes + +- `/api/auth` - Authentication endpoints +- `/api/[service]/bots` - Bot management for each service +- `/api/[service]/webhooks` - Webhook configuration + +### Page Structure + +- `/` - Dashboard/home page +- `/login` - Authentication page +- `/[...segment]` - Dynamic routing for CRUD operations + - `@create` - Create new entities + - `@detail` - View entity details + - `@edit` - Edit existing entities + +## Integration + +### Database Access + +Uses Kysely ORM for type-safe database queries: + +```typescript +import { db } from '@link-stack/database' + +const bots = await db + .selectFrom('bots') + .selectAll() + .execute() +``` + +### Authentication + +Integrated with NextAuth.js using database adapter: + +```typescript +import { authOptions } from '@link-stack/auth' +``` + +## Docker Support + +```bash +# Build image +docker build -t link-stack/bridge-frontend . + +# Run with docker-compose +docker-compose -f docker/compose/bridge.yml up +``` + +## Related Services + +- **bridge-worker**: Processes messages from configured channels +- **bridge-whatsapp**: WhatsApp-specific integration service +- **bridge-migrations**: Database schema management \ No newline at end of file diff --git a/apps/bridge-frontend/app/(login)/login/page.tsx b/apps/bridge-frontend/app/(login)/login/page.tsx new file mode 100644 index 0000000..8f79c27 --- /dev/null +++ b/apps/bridge-frontend/app/(login)/login/page.tsx @@ -0,0 +1,14 @@ +import { Metadata } from "next"; +import { getSession } from "next-auth/react"; +import { Login } from "@/app/_components/Login"; + +export const dynamic = "force-dynamic"; + +export const metadata: Metadata = { + title: "Login", +}; + +export default async function Page() { + const session = await getSession(); + return ; +} diff --git a/apps/bridge-frontend/app/(main)/[...segment]/@create/page.tsx b/apps/bridge-frontend/app/(main)/[...segment]/@create/page.tsx new file mode 100644 index 0000000..bc5b6b8 --- /dev/null +++ b/apps/bridge-frontend/app/(main)/[...segment]/@create/page.tsx @@ -0,0 +1,12 @@ +import { Create } from "@link-stack/bridge-ui"; + +type PageProps = { + params: Promise<{ segment: string[] }>; +}; + +export default async function Page({ params }: PageProps) { + const { segment } = await params; + const service = segment[0]; + + return ; +} diff --git a/apps/bridge-frontend/app/(main)/[...segment]/@detail/page.tsx b/apps/bridge-frontend/app/(main)/[...segment]/@detail/page.tsx new file mode 100644 index 0000000..e857b7d --- /dev/null +++ b/apps/bridge-frontend/app/(main)/[...segment]/@detail/page.tsx @@ -0,0 +1,28 @@ +import { db } from "@link-stack/bridge-common"; +import { serviceConfig, Detail } from "@link-stack/bridge-ui"; + +type PageProps = { + params: Promise<{ segment: string[] }>; +}; + +export default async function Page({ params }: PageProps) { + const { segment } = await params; + const service = segment[0]; + const id = segment?.[1]; + + if (!id) return null; + + const { + [service]: { table }, + } = serviceConfig; + + const row = await db + .selectFrom(table) + .selectAll() + .where("id", "=", id) + .executeTakeFirst(); + + if (!row) return null; + + return ; +} diff --git a/apps/bridge-frontend/app/(main)/[...segment]/@edit/page.tsx b/apps/bridge-frontend/app/(main)/[...segment]/@edit/page.tsx new file mode 100644 index 0000000..82c8052 --- /dev/null +++ b/apps/bridge-frontend/app/(main)/[...segment]/@edit/page.tsx @@ -0,0 +1,28 @@ +import { db } from "@link-stack/bridge-common"; +import { serviceConfig, Edit } from "@link-stack/bridge-ui"; + +type PageProps = { + params: Promise<{ segment: string[] }>; +}; + +export default async function Page({ params }: PageProps) { + const { segment } = await params; + const service = segment[0]; + const id = segment?.[1]; + + if (!id) return null; + + const { + [service]: { table }, + } = serviceConfig; + + const row = await db + .selectFrom(table) + .selectAll() + .where("id", "=", id) + .executeTakeFirst(); + + if (!row) return null; + + return ; +} diff --git a/apps/bridge-frontend/app/(main)/[...segment]/layout.tsx b/apps/bridge-frontend/app/(main)/[...segment]/layout.tsx new file mode 100644 index 0000000..c360a57 --- /dev/null +++ b/apps/bridge-frontend/app/(main)/[...segment]/layout.tsx @@ -0,0 +1,3 @@ +import { ServiceLayout } from "@link-stack/bridge-ui"; + +export default ServiceLayout; diff --git a/apps/bridge-frontend/app/(main)/[...segment]/page.tsx b/apps/bridge-frontend/app/(main)/[...segment]/page.tsx new file mode 100644 index 0000000..7b4fc03 --- /dev/null +++ b/apps/bridge-frontend/app/(main)/[...segment]/page.tsx @@ -0,0 +1,23 @@ +import { db } from "@link-stack/bridge-common"; +import { serviceConfig, List } from "@link-stack/bridge-ui"; + +type PageProps = { + params: Promise<{ + segment: string[]; + }>; +}; + +export default async function Page({ params }: PageProps) { + const { segment } = await params; + const service = segment[0]; + + if (!service) return null; + + const config = serviceConfig[service]; + + if (!config) return null; + + const rows = await db.selectFrom(config.table).selectAll().execute(); + + return ; +} diff --git a/apps/bridge-frontend/app/(main)/layout.tsx b/apps/bridge-frontend/app/(main)/layout.tsx new file mode 100644 index 0000000..32203a2 --- /dev/null +++ b/apps/bridge-frontend/app/(main)/layout.tsx @@ -0,0 +1,9 @@ +import { InternalLayout } from "@/app/_components/InternalLayout"; + +export default function Layout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return {children}; +} diff --git a/apps/bridge-frontend/app/(main)/page.tsx b/apps/bridge-frontend/app/(main)/page.tsx new file mode 100644 index 0000000..e01be47 --- /dev/null +++ b/apps/bridge-frontend/app/(main)/page.tsx @@ -0,0 +1,5 @@ +import { Home } from "@link-stack/bridge-ui"; + +export default function Page() { + return ; +} diff --git a/apps/bridge-frontend/app/_components/InternalLayout.tsx b/apps/bridge-frontend/app/_components/InternalLayout.tsx new file mode 100644 index 0000000..72985ad --- /dev/null +++ b/apps/bridge-frontend/app/_components/InternalLayout.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { FC, PropsWithChildren, useState } from "react"; +import { Grid } from "@mui/material"; +import { CssBaseline } from "@mui/material"; +import { AppRouterCacheProvider } from "@mui/material-nextjs/v14-appRouter"; +import { SessionProvider } from "next-auth/react"; +import { Sidebar } from "./Sidebar"; + +export const InternalLayout: FC = ({ children }) => { + const [open, setOpen] = useState(true); + + return ( + + + + + + + {children as any} + + + + + ); +}; diff --git a/apps/bridge-frontend/app/_components/Login.tsx b/apps/bridge-frontend/app/_components/Login.tsx new file mode 100644 index 0000000..8e40129 --- /dev/null +++ b/apps/bridge-frontend/app/_components/Login.tsx @@ -0,0 +1,185 @@ +"use client"; + +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 "@/app/_images/link-logo-small.png"; +import { colors, fonts } from "@link-stack/ui"; +import { useSearchParams } from "next/navigation"; + +type LoginProps = { + session: any; +}; + +export const Login: FC = ({ session }) => { + const origin = + 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 { poppins } = fonts; + 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 ( + + + + + + + Link logo + + + + + CDR Bridge + + + + + + {!session ? ( + + + {error ? ( + + + + {`${error} error`} + + + + ) : null} + + + signIn("google", { + callbackUrl: `${origin}`, + }) + } + > + + Sign in with Google + + + + + signIn("apple", { + callbackUrl: `${window.location.origin}`, + }) + } + > + + Sign in with Apple + + + + + ) : null} + {session ? ( + + {` ${session.user.name ?? session.user.email}.`} + + ) : null} + + + + + ); +}; diff --git a/apps/bridge-frontend/app/_components/Sidebar.tsx b/apps/bridge-frontend/app/_components/Sidebar.tsx new file mode 100644 index 0000000..31aaaaa --- /dev/null +++ b/apps/bridge-frontend/app/_components/Sidebar.tsx @@ -0,0 +1,399 @@ +"use client"; + +import { FC } from "react"; +import { + Box, + Grid, + Typography, + List, + ListItemButton, + ListItemIcon, + ListItemText, + ListItemSecondaryAction, + Drawer, +} from "@mui/material"; +import { + ExpandCircleDown as ExpandCircleDownIcon, + AccountCircle as AccountCircleIcon, + Chat as ChatIcon, + PermPhoneMsg as PhoneIcon, + WhatsApp as WhatsAppIcon, + Facebook as FacebookIcon, + AirlineStops as AirlineStopsIcon, + Logout as LogoutIcon, +} from "@mui/icons-material"; +import { usePathname } from "next/navigation"; +import Link from "next/link"; +import Image from "next/image"; +import { typography, fonts, Button } from "@link-stack/ui"; +import LinkLogo from "@/app/_images/link-logo-small.png"; +import { useSession, signOut } from "next-auth/react"; + +const openWidth = 270; +const closedWidth = 70; + +const MenuItem = ({ + name, + href, + Icon, + iconSize, + inset = false, + selected = false, + open = true, + badge, + target = "_self", +}: any) => ( + + + {iconSize > 0 ? ( + + + + + + ) : ( + + + + + )} + {open && ( + + {name} + + } + /> + )} + {badge && badge > 0 ? ( + + + {badge} + + + ) : null} + + +); + +interface SidebarProps { + open: boolean; + setOpen: (open: boolean) => void; +} + +export const Sidebar: FC = ({ open, setOpen }) => { + const pathname = usePathname(); + const { poppins } = fonts; + const { bodyLarge } = typography; + const { data: session } = useSession(); + const user = session?.user; + + const logout = () => { + signOut({ callbackUrl: "/login" }); + }; + + return ( + + { + setOpen!(!open); + }} + > + + + + + + + Link logo + + . + + {open && ( + + + CDR Bridge + + + )} + + + + + + + + + + + + + + + + {user?.image && ( + + + Profile image + + + )} + + + + {user?.email} + + + + - - { - popupState.close(); - signOut({ callbackUrl: "/" }); - }} - > - {t("signOut")} - - - - ); -}; diff --git a/apps/leafcutter/components/AppProvider.tsx b/apps/leafcutter/components/AppProvider.tsx deleted file mode 100644 index f829271..0000000 --- a/apps/leafcutter/components/AppProvider.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { - FC, - createContext, - useContext, - useReducer, - useState, - PropsWithChildren, -} from "react"; -import { colors, typography } from "styles/theme"; - -const basePath = process.env.GITLAB_CI - ? "/link/link-stack/apps/leafcutter" - : ""; -const imageURL = (image: any) => - typeof image === "string" ? `${basePath}${image}` : `${basePath}${image.src}`; - -const AppContext = createContext({ - colors, - typography, - imageURL, - query: null as any, - updateQuery: null as any, - updateQueryType: null as any, - replaceQuery: null as any, - clearQuery: null as any, - foundCount: 0, - setFoundCount: null as any, -}); - -export const AppProvider: FC = ({ children }) => { - const initialState = { - incidentType: { - display: "Incident Type", - queryType: "include", - values: [], - }, - relativeDate: { - display: "Relative Date", - queryType: null, - values: [], - }, - startDate: { - display: "Start Date", - queryType: null, - values: [], - }, - endDate: { - display: "End Date", - queryType: null, - values: [], - }, - targetedGroup: { - display: "Targeted Group", - queryType: "include", - values: [], - }, - platform: { - display: "Platform", - queryType: "include", - values: [], - }, - device: { - display: "Device", - queryType: "include", - values: [], - }, - service: { - display: "Service", - queryType: "include", - values: [], - }, - maker: { - display: "Maker", - queryType: "include", - values: [], - }, - country: { - display: "Country", - queryType: "include", - values: [], - }, - subregion: { - display: "Subregion", - queryType: "include", - values: [], - }, - continent: { - display: "Continent", - queryType: "include", - values: [], - }, - }; - const reducer = (state: any, action: any) => { - const key = action.payload?.[0]; - if (!key) { - throw new Error("Unknown key"); - } - const newState = { ...state }; - switch (action.type) { - case "UPDATE": - newState[key].values = action.payload[key].values; - return newState; - case "UPDATE_TYPE": - newState[key].queryType = action.payload[key].queryType; - return newState; - case "REPLACE": - return Object.keys(action.payload).reduce((acc: any, cur: string) => { - if (["startDate", "endDate"].includes(cur)) { - const rawDate = action.payload[cur].values[0]; - const date = new Date(rawDate); - acc[cur] = { - ...action.payload[cur], - values: rawDate && date ? [date] : [], - }; - } else { - acc[cur] = action.payload[cur]; - } - - return acc; - }, {}); - case "CLEAR": - return initialState; - default: - throw new Error("Unknown action type"); - } - }; - - const [query, dispatch] = useReducer(reducer, initialState); - const updateQuery = (payload: any) => dispatch({ type: "UPDATE", payload }); - const updateQueryType = (payload: any) => - dispatch({ type: "UPDATE_TYPE", payload }); - const replaceQuery = (payload: any) => dispatch({ type: "REPLACE", payload }); - const clearQuery = () => dispatch({ type: "CLEAR" }); - const [foundCount, setFoundCount] = useState(0); - - return ( - - {children} - - ); -}; - -export function useAppContext() { - return useContext(AppContext); -} diff --git a/apps/leafcutter/components/Button.tsx b/apps/leafcutter/components/Button.tsx deleted file mode 100644 index 923705c..0000000 --- a/apps/leafcutter/components/Button.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { FC } from "react"; -import Link from "next/link"; -import { Button as MUIButton } from "@mui/material"; -import { useAppContext } from "./AppProvider"; - -interface ButtonProps { - text: string; - color: string; - href: string; -} - -export const Button: FC = ({ text, color, href }) => { - const { - colors: { white, almostBlack }, - } = useAppContext(); - - return ( - - - {text} - - - ); -}; diff --git a/apps/leafcutter/components/Footer.tsx b/apps/leafcutter/components/Footer.tsx deleted file mode 100644 index 49ec23b..0000000 --- a/apps/leafcutter/components/Footer.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { FC } from "react"; -import { Container, Grid, Box, Button } from "@mui/material"; -import { useTranslate } from "react-polyglot"; -import Image from "next/legacy/image"; -import Link from "next/link"; -import leafcutterLogo from "images/leafcutter-logo.png"; -import footerLogo from "images/footer-logo.png"; -import twitterLogo from "images/twitter-logo.png"; -import gitlabLogo from "images/gitlab-logo.png"; -import { useAppContext } from "./AppProvider"; - -export const Footer: FC = () => { - const t = useTranslate(); - const { - colors: { white, leafcutterElectricBlue }, - typography: { bodySmall }, - } = useAppContext(); - const smallLinkStyles: any = { - ...bodySmall, - color: white, - textTransform: "none", - }; - - return ( - - - - - - CDR logo - - - - {t("contactUs")} - - - - - - - - - - - ©️ {t("copyright")} - - - - - - - - - - - - - - - - Gitlab logo - - - - - Twitter logo - - - - - - - ); -}; diff --git a/apps/leafcutter/components/GettingStartedDialog.tsx b/apps/leafcutter/components/GettingStartedDialog.tsx deleted file mode 100644 index 1e5f94c..0000000 --- a/apps/leafcutter/components/GettingStartedDialog.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { FC, useState } from "react"; -import { Dialog, Box, Grid, Checkbox, IconButton } from "@mui/material"; -import { Close as CloseIcon } from "@mui/icons-material"; -import { useRouter } from "next/router"; -import { useTranslate } from "react-polyglot"; -import { useAppContext } from "./AppProvider"; - -type CheckboxItemProps = { - title: string; - description: string; - checked: boolean; - onChange: () => void; -}; - -const CheckboxItem: FC = ({ - title, - description, - checked, - onChange, -}) => { - const { - typography: { p, small }, - } = useAppContext(); - - return ( - - - - - - - - {title} - - - {description} - - - - - ); -}; - -export const GettingStartedDialog: FC = () => { - const { - colors: { almostBlack }, - typography: { h4 }, - } = useAppContext(); - const t = useTranslate(); - const [completedItems, setCompletedItems] = useState([] as any[]); - const router = useRouter(); - const open = router.query.tooltip?.toString() === "checklist"; - const toggleCompletedItem = (item: any) => { - if (completedItems.includes(item)) { - setCompletedItems(completedItems.filter((i) => i !== item)); - } else { - setCompletedItems([...completedItems, item]); - } - }; - - return ( - - - - - - {t("getStartedChecklist")} - - - router.push(router.pathname)}> - - - - - - toggleCompletedItem("search")} - /> - toggleCompletedItem("create")} - /> - toggleCompletedItem("save")} - /> - toggleCompletedItem("export")} - /> - toggleCompletedItem("share")} - /> - - - - - ); -}; diff --git a/apps/leafcutter/components/HelpButton.tsx b/apps/leafcutter/components/HelpButton.tsx deleted file mode 100644 index a67b1a2..0000000 --- a/apps/leafcutter/components/HelpButton.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { FC, useState } from "react"; -import { useRouter } from "next/router"; -import { Button } from "@mui/material"; -import { QuestionMark as QuestionMarkIcon } from "@mui/icons-material"; -import { useAppContext } from "./AppProvider"; - -export const HelpButton: FC = () => { - const router = useRouter(); - const [helpActive, setHelpActive] = useState(false); - const { - colors: { leafcutterElectricBlue }, - } = useAppContext(); - const onClick = () => { - if (helpActive) { - router.push(router.pathname); - } else { - router.push("/?tooltip=welcome"); - } - setHelpActive(!helpActive); - }; - - return ( - - ); -}; diff --git a/apps/leafcutter/components/LanguageSelect.tsx b/apps/leafcutter/components/LanguageSelect.tsx deleted file mode 100644 index e8b4fbf..0000000 --- a/apps/leafcutter/components/LanguageSelect.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useRouter } from "next/router"; -import { IconButton, Menu, MenuItem, Box } from "@mui/material"; -import { KeyboardArrowDown as KeyboardArrowDownIcon } from "@mui/icons-material"; -import { - usePopupState, - bindTrigger, - bindMenu, -} from "material-ui-popup-state/hooks"; -import { useAppContext } from "./AppProvider"; -// import { Tooltip } from "./Tooltip"; - -export const LanguageSelect = () => { - const { - colors: { white, leafcutterElectricBlue }, - } = useAppContext(); - const router = useRouter(); - const locales: any = { en: "English", fr: "Français" }; - const popupState = usePopupState({ variant: "popover", popupId: "language" }); - - return ( - - - {locales[router.locale as any] ?? locales.en} - - - - {Object.keys(locales).map((locale) => ( - { - router.push(router.route, router.route, { locale }); - popupState.close(); - }} - > - {locales[locale]} - - ))} - - - ); -}; - -/* */ diff --git a/apps/leafcutter/components/Layout.tsx b/apps/leafcutter/components/Layout.tsx deleted file mode 100644 index 29a63fc..0000000 --- a/apps/leafcutter/components/Layout.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { FC, PropsWithChildren } from "react"; -import getConfig from "next/config"; -import { Grid, Container } from "@mui/material"; -import CookieConsent from "react-cookie-consent"; -import { useCookies } from "react-cookie"; -import { TopNav } from "./TopNav"; -import { Sidebar } from "./Sidebar"; -import { GettingStartedDialog } from "./GettingStartedDialog"; -import { useAppContext } from "./AppProvider"; -// import { Footer } from "./Footer"; - -type LayoutProps = PropsWithChildren<{ - embedded?: boolean; -}>; - -export const Layout: FC = ({ - embedded = false, - children, -}: any) => { - const [cookies, setCookie] = useCookies(["cookieConsent"]); - const consentGranted = cookies.cookieConsent === "true"; - const { - colors: { - white, - almostBlack, - leafcutterElectricBlue, - cdrLinkOrange, - helpYellow, - }, - } = useAppContext(); - - return ( - <> - - {!embedded && ( - - - - )} - {!embedded && } - - {children} - - - {!consentGranted ? ( - setCookie("cookieConsent", "true", { path: "/" })} - buttonStyle={{ - borderRadius: 500, - backgroundColor: cdrLinkOrange, - color: white, - textTransform: "uppercase", - padding: "10px 20px", - fontWeight: "bold", - fontSize: 14, - }} - > - Leafcutter uses cookies for core funtionality. - - ) : null} - - - ); -}; diff --git a/apps/leafcutter/components/LiveDataViewer.tsx b/apps/leafcutter/components/LiveDataViewer.tsx deleted file mode 100644 index 9ff9436..0000000 --- a/apps/leafcutter/components/LiveDataViewer.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { FC, useEffect, useState } from "react"; -import { useAppContext } from "./AppProvider"; -import { RawDataViewer } from "./RawDataViewer"; - -export const LiveDataViewer: FC = () => { - const { query, setFoundCount } = useAppContext(); - const [rows, setRows] = useState([]); - const searchQuery = encodeURI(JSON.stringify(query)); - useEffect(() => { - const fetchData = async () => { - const result = await fetch( - `/api/visualizations/query?searchQuery=${searchQuery}` - ); - const json = await result.json(); - setRows(json); - setFoundCount(json?.length ?? 0); - }; - fetchData(); - }, [searchQuery, setFoundCount]); - - return ; -}; diff --git a/apps/leafcutter/components/MetricSelectCard.tsx b/apps/leafcutter/components/MetricSelectCard.tsx deleted file mode 100644 index 60a08f5..0000000 --- a/apps/leafcutter/components/MetricSelectCard.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { FC, useState } from "react"; -import { Card, Grid } from "@mui/material"; -import { - PrivacyTip as PrivacyTipIcon, - PhoneIphone as PhoneIphoneIcon, - Map as MapIcon, - Group as GroupIcon, - DateRange as DateRangeIcon, - Public as PublicIcon, -} from "@mui/icons-material"; -import { VisualizationDetailDialog } from "./VisualizationDetailDialog"; -import { useAppContext } from "./AppProvider"; - -interface MetricSelectCardProps { - visualizationID: string; - metricType: string; - title: string; - description: string; - enabled: boolean; -} - -export const MetricSelectCard: FC = ({ - visualizationID, - metricType, - title, - description, - enabled, -}) => { - const [open, setOpen] = useState(false); - const closeDialog = () => setOpen(false); - const [dialogParams, setDialogParams] = useState({}); - const { - typography: { small }, - colors: { white, leafcutterElectricBlue, cdrLinkOrange }, - query, - } = useAppContext(); - /* const images = { - actor: PrivacyTipIcon, - incidenttype: PrivacyTipIcon, - channel: PrivacyTipIcon, - date: DateRangeIcon, - targetedgroup: GroupIcon, - impactedtechnology: PhoneIphoneIcon, - location: MapIcon, - }; */ - - const createAndOpen = async () => { - const createParams = { - visualizationID, - title, - description, - query, - }; - const result: any = await fetch(`/api/visualizations/create`, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify(createParams), - }); - - const { id } = await result.json(); - const params = { - id, - title: createParams.title, - description: createParams.description, - url: `/app/visualize?security_tenant=private#/edit/${id}?embed=true`, - }; - setDialogParams(params); - setOpen(true); - }; - - return ( - <> - - - - {title} - - - {metricType === "impactedtechnology" && ( - - )} - {metricType === "region" && ( - - )} - {metricType === "continent" && ( - - )} - {metricType === "country" && ( - - )} - {metricType === "targetedgroup" && ( - - )} - {metricType === "incidenttype" && ( - - )} - {metricType === "date" && ( - - )} - - - - {open ? ( - - ) : null} - - ); -}; diff --git a/apps/leafcutter/components/PageHeader.tsx b/apps/leafcutter/components/PageHeader.tsx deleted file mode 100644 index 46c31d2..0000000 --- a/apps/leafcutter/components/PageHeader.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* eslint-disable react/require-default-props */ -import { FC, PropsWithChildren } from "react"; -import { Box } from "@mui/material"; -import { useAppContext } from "./AppProvider"; - -type PageHeaderProps = PropsWithChildren<{ - backgroundColor: string; - sx?: any; -}>; - -export const PageHeader: FC = ({ - backgroundColor, - sx = {}, - children, -}: any) => { - const { - colors: { white }, - } = useAppContext(); - - return ( - - {children} - - ); -}; diff --git a/apps/leafcutter/components/QueryBuilder.tsx b/apps/leafcutter/components/QueryBuilder.tsx deleted file mode 100644 index bbf8846..0000000 --- a/apps/leafcutter/components/QueryBuilder.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import { FC, useState } from "react"; -import { - Box, - Grid, - Dialog, - DialogActions, - Button, - DialogContent, -} from "@mui/material"; -import { - PrivacyTip as PrivacyTipIcon, - DateRange as DateRangeIcon, - PhoneIphone as PhoneIphoneIcon, - Map as MapIcon, - Group as GroupIcon, -} from "@mui/icons-material"; -import { useTranslate } from "react-polyglot"; -import taxonomy from "config/taxonomy.json"; -import { QueryBuilderSection } from "./QueryBuilderSection"; -import { QueryListSelector } from "./QueryListSelector"; -import { QueryDateRangeSelector } from "./QueryDateRangeSelector"; -import { useAppContext } from "./AppProvider"; -import { Tooltip } from "./Tooltip"; - -interface QueryBuilderProps { } - -export const QueryBuilder: FC = () => { - const t = useTranslate(); - const [dialogOpen, setDialogOpen] = useState(false); - const { - typography: { p }, - colors: { leafcutterElectricBlue, mediumGray, almostBlack }, - } = useAppContext(); - - const openAdvancedOptions = () => { - setDialogOpen(false); - window.open(`/app/visualize`, "_ blank"); - }; - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {t("fullInterfaceWillOpen")} - - - - - - - - - - - - - - ); -}; diff --git a/apps/leafcutter/components/QueryBuilderSection.tsx b/apps/leafcutter/components/QueryBuilderSection.tsx deleted file mode 100644 index 99dad2e..0000000 --- a/apps/leafcutter/components/QueryBuilderSection.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import { FC, PropsWithChildren, useState } from "react"; -import { - Box, - Grid, - Accordion, - AccordionSummary, - AccordionDetails, - Button, - ButtonGroup, - IconButton, - Tooltip as MUITooltip, -} from "@mui/material"; -import { useTranslate } from "react-polyglot"; -import { - ExpandMore as ExpandMoreIcon, - Help as HelpIcon, -} from "@mui/icons-material"; -import { useAppContext } from "./AppProvider"; - -interface QueryBuilderSectionProps { - name: string; - keyName: string; - children: any; - Image: any; - width: number; - // eslint-disable-next-line react/require-default-props - showQueryType?: boolean; - tooltipTitle: string; - tooltipDescription: string; -} - -type TooltipProps = PropsWithChildren<{ - title: string; - description: string; - children: any; - open: boolean; -}>; - -const Tooltip: FC = ({ title, description, children, open }) => { - const { - colors: { white, leafcutterElectricBlue, almostBlack }, - typography: { h5, small }, - } = useAppContext(); - - return ( - - - - {title} - - - {description} - - - - } - arrow - placement="top" - componentsProps={{ - tooltip: { - sx: { - backgroundColor: white, - boxShadow: "0px 6px 8px rgba(0,0,0,0.5)", - }, - }, - arrow: { - sx: { - color: "white", - fontSize: "22px", - }, - }, - }} - > - {children} - - ); -}; - -export const QueryBuilderSection: FC = ({ - name, - keyName, - children, - Image, - width, - showQueryType = false, - tooltipTitle, - tooltipDescription, -}) => { - const t = useTranslate(); - const [queryType, setQueryType] = useState("include"); - const [showTooltip, setShowTooltip] = useState(false); - const { - colors: { white, leafcutterElectricBlue, warningPink, almostBlack }, - typography: { h6, small }, - updateQueryType, - } = useAppContext(); - const updateType = (type: string) => { - setQueryType(type); - updateQueryType({ - [keyName]: { queryType: type }, - }); - }; - - const minHeight = "42px"; - const maxHeight = "42px"; - - return ( - - - } - sx={{ - backgroundColor: leafcutterElectricBlue, - height: "14px", - minHeight, - maxHeight, - "&.Mui-expanded": { - minHeight, - maxHeight, - }, - }} - > - - - - - - - {name} - - - - - setShowTooltip(true)} - onMouseLeave={() => setShowTooltip(false)} - > - - - - - - - - {showQueryType ? ( - - - - - - - - - these items: - - - ) : null} - {children} - - - - ); -}; diff --git a/apps/leafcutter/components/QueryDateRangeSelector.tsx b/apps/leafcutter/components/QueryDateRangeSelector.tsx deleted file mode 100644 index bc8c82e..0000000 --- a/apps/leafcutter/components/QueryDateRangeSelector.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { FC, useState, useEffect } from "react"; -import { Box, Grid, TextField, Select, MenuItem } from "@mui/material"; -import { DatePicker } from "@mui/x-date-pickers-pro"; -import { useTranslate } from "react-polyglot"; -import { useAppContext } from "./AppProvider"; - -interface QueryDateRangeSelectorProps {} - -export const QueryDateRangeSelector: FC = () => { - const t = useTranslate(); - const [relativeDate, setRelativeDate] = useState(""); - const [startDate, setStartDate] = useState(null); - const [endDate, setEndDate] = useState(null); - const { updateQuery, query } = useAppContext(); - useEffect(() => { - setStartDate(query.startDate.values[0] ?? null); - setEndDate(query.endDate.values[0] ?? null); - setRelativeDate(query.relativeDate.values[0] ?? ""); - }, [query, setStartDate, setEndDate, setRelativeDate]); - - return ( - - - - - - - – or – - - - { - setStartDate(date); - updateQuery({ - startDate: { values: [date] }, - }); - }} - // @ts-ignore - renderInput={(params) => ( - - )} - /> - - - { - setEndDate(date); - updateQuery({ - endDate: { values: [date] }, - }); - }} - // @ts-ignore - renderInput={(params) => ( - - )} - /> - - - - ); -}; diff --git a/apps/leafcutter/components/QueryListSelector.tsx b/apps/leafcutter/components/QueryListSelector.tsx deleted file mode 100644 index 4602fd6..0000000 --- a/apps/leafcutter/components/QueryListSelector.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { FC, useState, useEffect } from "react"; -import { Box, Grid, Tooltip } from "@mui/material"; -import { DataGridPro, GridColDef } from "@mui/x-data-grid-pro"; -import { useAppContext } from "./AppProvider"; - -interface QueryListSelectorProps { - title: string; - keyName: string; - values: any; - width: number; -} - -export const QueryListSelector: FC = ({ - title, - keyName, - values, - width, -}) => { - const [selectionModel, setSelectionModel] = useState([] as any[]); - const { - colors: { leafcutterLightBlue, pink, leafcutterElectricBlue, warningPink }, - typography: { small }, - query, - updateQuery, - } = useAppContext(); - const isExclude = query[keyName]?.queryType === "exclude"; - const columns: GridColDef[] = [ - { - field: "value", - renderHeader: () => ( - {title} - ), - renderCell: ({ value, row }) => ( - - {value} - - ), - editable: false, - flex: 1, - }, - ]; - const rows = Object.keys(values).map((k) => ({ - id: k, - value: values[k].display, - description: values[k].description, - category: values[k].category, - })); - - useEffect(() => { - setSelectionModel(query[keyName].values); - }, [query, keyName, setSelectionModel]); - - return ( - - - - - { - setSelectionModel(newSelectionModel); - updateQuery({ - [keyName]: { values: newSelectionModel }, - }); - }} - rowSelectionModel={selectionModel} - /> - - - - - ); -}; diff --git a/apps/leafcutter/components/QueryText.tsx b/apps/leafcutter/components/QueryText.tsx deleted file mode 100644 index cffc7d0..0000000 --- a/apps/leafcutter/components/QueryText.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { FC, useState, useEffect } from "react"; -import { Box, Grid } from "@mui/material"; -import { useTranslate } from "react-polyglot"; -import taxonomy from "config/taxonomy.json"; -import { colors } from "styles/theme"; -import { useAppContext } from "./AppProvider"; - -export const QueryText: FC = () => { - const t = useTranslate(); - const { - typography: { h6 }, - query: q, - } = useAppContext(); - - const displayNames: any = { - incidentType: t("incidentType"), - startDate: t("startDate"), - endDate: t("endDate"), - relativeDate: t("relativeDate"), - targetedGroup: t("targetedGroup"), - platform: t("platform"), - device: t("device"), - service: t("service"), - maker: t("maker"), - country: t("country"), - subregion: t("subregion"), - continent: t("continent"), - }; - - const createClause = (query: any, key: string) => { - const { values, queryType } = query[key]; - const color = - queryType === "include" - ? colors.leafcutterElectricBlue - : colors.warningPink; - - if (values.length > 0) { - return `where ${ - displayNames[key] - } ${ - queryType === "include" ? ` ${t("is")} ` : ` ${t("isNot")} ` - } ${values - .map( - // @ts-expect-error - (value: string) => `${taxonomy[key]?.[value]?.display ?? ""}` - ) - .join(` ${t("or")} `)}`; - } - - return null; - }; - const createDateClause = (query: any, key: string) => { - const { values } = query[key]; - const color = colors.leafcutterElectricBlue; - if (values.length > 0) { - const range = key === "startDate" ? t("onOrAfter") : t("onOrBefore"); - return `${t("where")} ${ - displayNames[key] - } is ${range} ${values[0]?.toLocaleDateString()}`; - } - return null; - }; - const createRelativeDateClause = (query: any, key: string) => { - const { values } = query[key]; - const color = colors.leafcutterElectricBlue; - - if (query[key].values.length > 0) { - const range = t("onOrAfter"); - return `${t("where")} ${ - displayNames[key] - } is ${range} ${values[0]} days ago`; - } - return null; - }; - - const [queryText, setQueryText] = useState(t("findAllIncidents")); - useEffect(() => { - const generateQueryText = (query: any) => { - const incidentClause = createClause(query, "incidentType"); - const startDateClause = createDateClause(query, "startDate"); - const endDateClause = createDateClause(query, "endDate"); - const relativeDateClause = createRelativeDateClause( - query, - "relativeDate" - ); - const targetedGroupClause = createClause(query, "targetedGroup"); - const platformClause = createClause(query, "platform"); - const deviceClause = createClause(query, "device"); - const serviceClause = createClause(query, "service"); - const makerClause = createClause(query, "maker"); - const countryClause = createClause(query, "country"); - const subregionClause = createClause(query, "subregion"); - const continentClause = createClause(query, "continent"); - const joinedClauses = [ - incidentClause, - startDateClause, - endDateClause, - relativeDateClause, - targetedGroupClause, - platformClause, - deviceClause, - serviceClause, - makerClause, - countryClause, - subregionClause, - continentClause, - ] - .filter((clause) => clause !== null) - .join(" and "); - - return `${t("findAllIncidents")} ${joinedClauses}`; - }; - const text = generateQueryText(q); - setQueryText(text); - }, [q]); - - return ( - - - - - - ); -}; diff --git a/apps/leafcutter/components/Question.tsx b/apps/leafcutter/components/Question.tsx deleted file mode 100644 index 50af2a9..0000000 --- a/apps/leafcutter/components/Question.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { FC, useState } from "react"; -import ReactMarkdown from "react-markdown"; -import { - Grid, - Box, - Accordion, - AccordionSummary, - AccordionDetails, -} from "@mui/material"; -import { - ChevronRight as ChevronRightIcon, - ExpandMore as ExpandMoreIcon, - Circle as CircleIcon, -} from "@mui/icons-material"; -import { useAppContext } from "./AppProvider"; - -interface QuestionProps { - question: string; - answer: string; -} - -export const Question: FC = ({ question, answer }) => { - const [expanded, setExpanded] = useState(false); - const { - colors: { lavender, darkLavender }, - typography: { h5, p }, - } = useAppContext(); - - return ( - setExpanded(!expanded)} - elevation={0} - sx={{ "::before": { display: "none" } }} - > - - - - - {question} - - {expanded ? ( - - ) : ( - - )} - - - - - {answer} - - - - ); -}; diff --git a/apps/leafcutter/components/RawDataViewer.tsx b/apps/leafcutter/components/RawDataViewer.tsx deleted file mode 100644 index bad2385..0000000 --- a/apps/leafcutter/components/RawDataViewer.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { FC } from "react"; -import { Box, Grid } from "@mui/material"; -import { DataGridPro } from "@mui/x-data-grid-pro"; -import { useTranslate } from "react-polyglot"; - -interface RawDataViewerProps { - rows: any[]; - height: number; -} - -export const RawDataViewer: FC = ({ rows, height }) => { - const t = useTranslate(); - const columns = [ - { - field: "date", - headerName: t("date"), - editable: false, - flex: 0.7, - valueFormatter: ({ value }: any) => new Date(value).toLocaleDateString(), - }, - { - field: "incident", - headerName: t("incident"), - editable: false, - flex: 1, - }, - { - field: "technology", - headerName: t("technology"), - editable: false, - flex: 0.8, - }, - { - field: "targeted_group", - headerName: t("targetedGroup"), - editable: false, - flex: 1.3, - }, - { - field: "country", - headerName: t("country"), - editable: false, - flex: 1, - }, - { - field: "region", - headerName: t("subregion"), - editable: false, - flex: 1, - }, - { - field: "continent", - headerName: t("continent"), - editable: false, - flex: 1, - }, - ]; - - return ( - - e.stopPropagation()} - > - - - - - - - - ); -}; diff --git a/apps/leafcutter/components/Sidebar.tsx b/apps/leafcutter/components/Sidebar.tsx deleted file mode 100644 index 136b371..0000000 --- a/apps/leafcutter/components/Sidebar.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import { FC } from "react"; -import DashboardMenuIcon from "images/dashboard-menu.png"; -import AboutMenuIcon from "images/about-menu.png"; -import TrendsMenuIcon from "images/trends-menu.png"; -import SearchCreateMenuIcon from "images/search-create-menu.png"; -import FAQMenuIcon from "images/faq-menu.png"; -import Image from "next/legacy/image"; -import { - Box, - Grid, - Typography, - List, - ListItem, - ListItemIcon, - ListItemText, - Drawer, -} from "@mui/material"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { useTranslate } from "react-polyglot"; -import { useAppContext } from "components/AppProvider"; -import { Tooltip } from "components/Tooltip"; -// import { ArrowCircleRight as ArrowCircleRightIcon } from "@mui/icons-material"; - -const MenuItem = ({ - name, - href, - selected, - icon, - iconSize, -}: // tooltipTitle, -// tooltipDescription, -{ - name: string; - href: string; - selected: boolean; - icon: any; - iconSize: number; - // tooltipTitle: string; - // tooltipDescription: string; -}) => { - const { - colors: { leafcutterLightBlue, black }, - } = useAppContext(); - - return ( - - - - - - - - - {name} - - } - /> - - - - ); -}; - -interface SidebarProps { - open: boolean; -} - -export const Sidebar: FC = ({ open }) => { - const t = useTranslate(); - const router = useRouter(); - const section = router.pathname.split("/")[1]; - const { - colors: { white }, // leafcutterElectricBlue, leafcutterLightBlue, - } = useAppContext(); - - // const [recentUpdates, setRecentUpdates] = useState([]); - - /* - useEffect(() => { - const getRecentUpdates = async () => { - const result = await fetch(`/api/trends/recent`); - const json = await result.json(); - setRecentUpdates(json); - }; - getRecentUpdates(); - }, []); - */ - - return ( - - - - - - - - - - - - - - {/* - - - - {t("recentUpdatesTitle")} - - {recentUpdates.map((trend, index) => ( - - - - {trend.title} - - - {trend.description}{" "} - - - - - ))} - - */} - - - ); -}; diff --git a/apps/leafcutter/components/Tooltip.tsx b/apps/leafcutter/components/Tooltip.tsx deleted file mode 100644 index b541055..0000000 --- a/apps/leafcutter/components/Tooltip.tsx +++ /dev/null @@ -1,156 +0,0 @@ -/* eslint-disable react/require-default-props */ -import { FC } from "react"; -import { useRouter } from "next/router"; -import { - Box, - Grid, - Tooltip as MUITooltip, - Button, - IconButton, -} from "@mui/material"; -import { Close as CloseIcon } from "@mui/icons-material"; -import { useTranslate } from "react-polyglot"; -import { useAppContext } from "./AppProvider"; - -interface TooltipProps { - title: string; - description: string; - placement: any; - tooltipID: string; - nextURL?: string; - previousURL?: string; - children: any; -} - -export const Tooltip: FC = ({ - title, - description, - placement, - tooltipID, - children, - previousURL = null, - nextURL = null, - // eslint-disable-next-line arrow-body-style -}) => { - const t = useTranslate(); - const { - typography: { p, small }, - colors: { white, leafcutterElectricBlue, almostBlack }, - } = useAppContext(); - const router = useRouter(); - const activeTooltip = router.query.tooltip?.toString(); - const open = activeTooltip === tooltipID; - const showNavigation = true; - - return ( - - - - router.push(router.pathname)}> - - - - - - - - - {title} - - - - {description} - - - - {showNavigation ? ( - - - {previousURL ? ( - - ) : null} - - - {nextURL ? ( - - ) : ( - - )} - - - ) : null} - - } - arrow - placement={placement} - sx={{ opacity: 0.9 }} - componentsProps={{ - tooltip: { - sx: { - opacity: 1.0, - backgroundColor: white, - color: leafcutterElectricBlue, - boxShadow: "0px 6px 20px rgba(0,0,0,0.25)", - }, - }, - arrow: { - sx: { opacity: 1.0, fontSize: "22px", color: white }, - }, - }} - > - {children} - - ); -}; diff --git a/apps/leafcutter/components/TopNav.tsx b/apps/leafcutter/components/TopNav.tsx deleted file mode 100644 index f21af59..0000000 --- a/apps/leafcutter/components/TopNav.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { FC } from "react"; -import Link from "next/link"; -import Image from "next/legacy/image"; -import { AppBar, Grid, Box } from "@mui/material"; -import { useTranslate } from "react-polyglot"; -import LeafcutterLogo from "images/leafcutter-logo.png"; -import { AccountButton } from "components/AccountButton"; -import { HelpButton } from "components/HelpButton"; -import { Tooltip } from "components/Tooltip"; -import { useAppContext } from "./AppProvider"; -// import { LanguageSelect } from "./LanguageSelect"; - -export const TopNav: FC = () => { - const t = useTranslate(); - const { - colors: { white, leafcutterElectricBlue, cdrLinkOrange }, - typography: { h5, h6 }, - } = useAppContext(); - - return ( - - - - - - - - - - - Leafcutter - - - - - A Project of Center for Digital Resilience - - - - - - - - - - {/* - - - - */} - - - - - - - - ); -}; diff --git a/apps/leafcutter/components/VisualizationBuilder.tsx b/apps/leafcutter/components/VisualizationBuilder.tsx deleted file mode 100644 index 81f2295..0000000 --- a/apps/leafcutter/components/VisualizationBuilder.tsx +++ /dev/null @@ -1,371 +0,0 @@ -import { FC, useState, useEffect } from "react"; -import { - Box, - Button, - Grid, - Popover, - Accordion, - AccordionSummary, - AccordionDetails, - Dialog, - Divider, - Paper, - MenuList, - MenuItem, - ListItemText, - ListItemIcon, - TextField, -} from "@mui/material"; -import { - ExpandMore as ExpandMoreIcon, - AddCircleOutline as AddCircleOutlineIcon, - SavedSearch as SavedSearchIcon, - RemoveCircle as RemoveCircleIcon, -} from "@mui/icons-material"; -import { useTranslate } from "react-polyglot"; -import { QueryBuilder } from "components/QueryBuilder"; -import { QueryText } from "components/QueryText"; -import { LiveDataViewer } from "components/LiveDataViewer"; -import { Tooltip } from "components/Tooltip"; -import visualizationMap from "config/visualizationMap.json"; -import { VisualizationSelectCard } from "./VisualizationSelectCard"; -import { MetricSelectCard } from "./MetricSelectCard"; -import { useAppContext } from "./AppProvider"; - -interface VisualizationBuilderProps { - templates: any[]; -} - -export const VisualizationBuilder: FC = ({ - templates, -}) => { - const t = useTranslate(); - const { - typography: { h4 }, - colors: { white, leafcutterElectricBlue, cdrLinkOrange }, - foundCount, - query, - replaceQuery, - clearQuery, - } = useAppContext(); - const { visualizations } = visualizationMap; - const [selectedVisualizationType, setSelectedVisualizationType] = useState( - null as any - ); - const toggleSelectedVisualizationType = (visualizationType: string) => { - if (visualizationType === selectedVisualizationType) { - setSelectedVisualizationType(null); - } else { - setSelectedVisualizationType(visualizationType); - } - }; - const [dialogOpen, setDialogOpen] = useState(false); - const [savedSearches, setSavedSearches] = useState([]); - const [savedSearchName, setSavedSearchName] = useState(""); - const [anchorEl, setAnchorEl] = useState(null); - - const updateSearches = async () => { - const result = await fetch("/api/searches/list"); - const existingSearches = await result.json(); - setSavedSearches(existingSearches); - }; - useEffect(() => { - updateSearches(); - }, [setSavedSearches]); - - const showSavedSearchPopup = (event: any) => { - setAnchorEl(event.currentTarget); - }; - const handleClose = () => { - setSavedSearchName(""); - setAnchorEl(null); - }; - const closeDialog = () => { - setDialogOpen(false); - }; - const createSavedSearch = async (name: string, q: any) => { - await fetch("/api/searches/create", { - method: "POST", - body: JSON.stringify({ name, query: q }), - }); - await updateSearches(); - handleClose(); - closeDialog(); - }; - - const deleteSavedSearch = async (name: string) => { - await fetch("/api/searches/delete", { - method: "POST", - body: JSON.stringify({ name }), - }); - await updateSearches(); - closeDialog(); - }; - - const updateSearch = (name: string) => { - handleClose(); - closeDialog(); - const found: any = savedSearches.find( - (search: any) => search.name === name - ); - replaceQuery(found?.query); - }; - - const clearSearch = () => clearQuery(); - - const open = Boolean(anchorEl); - const elementID = open ? "simple-popover" : undefined; - const [queryExpanded, setQueryExpanded] = useState(true); - const [resultsExpanded, setResultsExpanded] = useState(false); - const minHeight = "42px"; - const maxHeight = "42px"; - const summaryStyles = { - backgroundColor: leafcutterElectricBlue, - height: "14px", - minHeight, - maxHeight, - "&.Mui-expanded": { - minHeight, - maxHeight, - }, - }; - const buttonStyles = { - fontFamily: "Poppins, sans-serif", - fontWeight: 700, - color: `${white} !important`, - borderRadius: 999, - backgroundColor: leafcutterElectricBlue, - padding: "6px 30px", - margin: "20px 0px", - whiteSpace: "nowrap", - }; - - return ( - - - - - - setSavedSearchName(e.target.value)} - /> - - - - - - - - - - - - - - - - Search Criteria - - - - - - - - - { - handleClose(); - setDialogOpen(true); - }} - > - - - - {t("saveCurrentSearch")} - - - {savedSearches.map((savedSearch: any) => ( - updateSearch(savedSearch.name)} - > - - {savedSearch.name} - deleteSavedSearch(savedSearch.name)} - sx={{ p: 0, m: 0, zIndex: 100 }} - > - - - - ))} - - - - - - - setQueryExpanded(!queryExpanded)} - > - } - sx={summaryStyles} - > - {t("query")} - - - - - - setResultsExpanded(!resultsExpanded)} - > - } - sx={summaryStyles} - > - {`${t( - "results" - )} (${foundCount})`} - - - - - - - - - {t("selectVisualization")}: - - - {Object.keys(visualizations).map((key: string) => ( - - ))} - - {t("selectFieldVisualize")}: - - {templates - .filter( - (template: any) => template.type === selectedVisualizationType - ) - .map((template: any) => { - const { id, type, title, description } = template; - const cleanTitle = title - .replace("Templated", "") - // @ts-expect-error - .replace(visualizations[type].name, ""); - const metricType = cleanTitle.replace(/\s/g, "").toLowerCase(); - return ( - - ); - })} - - - ); -}; diff --git a/apps/leafcutter/components/VisualizationCard.tsx b/apps/leafcutter/components/VisualizationCard.tsx deleted file mode 100644 index 5d63bd7..0000000 --- a/apps/leafcutter/components/VisualizationCard.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { FC, useState } from "react"; -import { Grid, Card, Box } from "@mui/material"; -import Iframe from "react-iframe"; -import { useAppContext } from "components/AppProvider"; -import { VisualizationDetailDialog } from "components/VisualizationDetailDialog"; - -interface VisualizationCardProps { - id: string; - title: string; - description: string; - url: string; -} - -export const VisualizationCard: FC = ({ - id, - title, - description, - url, -}) => { - const [open, setOpen] = useState(false); - const closeDialog = () => setOpen(false); - const { - typography: { h4, p }, - colors: { leafcutterLightBlue, leafcutterElectricBlue }, - } = useAppContext(); - const finalURL = `${process.env.NEXT_PUBLIC_NEXTAUTH_URL}${url}&_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!t%2Cvalue%3A0)%2Ctime%3A(from%3Anow-3y%2Cto%3Anow))`; - - return ( - <> - - setOpen(true)} - > - -