├── .editorconfig ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .gitmodules ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── jsconfig.json ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── favicon │ ├── favicon.ico │ ├── favicon.png │ └── favicon.svg └── img │ ├── charts │ ├── area-chart.png │ ├── bar-chart.png │ ├── counter-chart.png │ ├── pie-chart.png │ └── table-chart.png │ ├── databases │ ├── mysql.svg │ ├── planetscale.svg │ ├── postgres.svg │ └── supabase.svg │ ├── full-logo.png │ └── supaboard.svg ├── src ├── app │ ├── (app) │ │ ├── contacts │ │ │ ├── action.js │ │ │ └── import │ │ │ │ └── page.js │ │ ├── dashboards │ │ │ ├── [id] │ │ │ │ ├── add-chart │ │ │ │ │ └── page.js │ │ │ │ ├── loading.js │ │ │ │ └── page.js │ │ │ ├── actions.js │ │ │ ├── loading.js │ │ │ └── page.js │ │ ├── databases │ │ │ ├── [id] │ │ │ │ ├── loading.js │ │ │ │ └── page.js │ │ │ ├── actions.js │ │ │ ├── new │ │ │ │ └── page.js │ │ │ └── page.js │ │ ├── layout.js │ │ ├── overview │ │ │ └── page.js │ │ ├── segments │ │ │ ├── [id] │ │ │ │ └── page.js │ │ │ ├── actions.js │ │ │ ├── new │ │ │ │ ├── layout.js │ │ │ │ └── page.js │ │ │ └── page.js │ │ ├── settings │ │ │ ├── billing │ │ │ │ └── page.js │ │ │ ├── layout.js │ │ │ ├── page.js │ │ │ └── workspace │ │ │ │ ├── [account_id] │ │ │ │ ├── actions.js │ │ │ │ └── page.js │ │ │ │ ├── new │ │ │ │ ├── actions.js │ │ │ │ └── page.js │ │ │ │ └── page.js │ │ ├── setup │ │ │ ├── page.js │ │ │ └── state │ │ │ │ └── route.js │ │ ├── thank-you │ │ │ └── page.js │ │ └── workflows │ │ │ └── page.js │ ├── (auth) │ │ ├── auth │ │ │ ├── callback │ │ │ │ └── route.js │ │ │ └── signout │ │ │ │ └── route.js │ │ ├── layout.js │ │ ├── login │ │ │ └── page.js │ │ └── register │ │ │ └── page.js │ ├── (public) │ │ └── public │ │ │ ├── dashboard │ │ │ └── [hash] │ │ │ │ └── page.js │ │ │ └── layout.js │ ├── api │ │ ├── contacts │ │ │ ├── route.js │ │ │ └── segments │ │ │ │ └── route.js │ │ ├── counts │ │ │ └── route.js │ │ ├── dashboards │ │ │ ├── [uuid] │ │ │ │ ├── charts │ │ │ │ │ └── [chart_id] │ │ │ │ │ │ └── route.js │ │ │ │ └── route.js │ │ │ ├── data │ │ │ │ └── route.js │ │ │ ├── hash │ │ │ │ └── [hash] │ │ │ │ │ └── route.js │ │ │ └── route.js │ │ ├── databases │ │ │ ├── [id] │ │ │ │ └── route.js │ │ │ └── route.js │ │ ├── externaldb │ │ │ └── [id] │ │ │ │ └── route.js │ │ ├── segments │ │ │ ├── [id] │ │ │ │ └── route.js │ │ │ └── route.js │ │ ├── settings │ │ │ └── route.js │ │ └── webhooks │ │ │ └── stripe │ │ │ └── route.js │ ├── favicon.ico │ ├── globals.css │ ├── layout.js │ └── opengraph-image.png ├── components │ ├── Loading.js │ ├── Sidebar.js │ ├── Skeleton.js │ ├── UpgradeModal.js │ ├── UserMenu.js │ ├── WorkspaceSwitcher.js │ ├── auth │ │ └── AuthForm.js │ ├── brand │ │ └── Logo.js │ ├── contacts │ │ └── steps │ │ │ └── SelectContacts.js │ ├── dashboards │ │ ├── DashboardDeleteModal.js │ │ ├── DashboardEditModal.js │ │ ├── DashboardNewModal.js │ │ ├── DashboardPublicModal.js │ │ ├── DashboardSwitcher.js │ │ ├── NoDashboards.js │ │ ├── charts │ │ │ ├── AreaChart.js │ │ │ ├── BarChart.js │ │ │ ├── ChartEditModal.js │ │ │ ├── ChartError.js │ │ │ ├── ChartErrorBoundary.js │ │ │ ├── ChartHeader.js │ │ │ ├── CounterChart.js │ │ │ ├── PieChart.js │ │ │ └── TableChart.js │ │ └── steps │ │ │ ├── ChartDataTable.js │ │ │ ├── ChartDatabase.js │ │ │ ├── ChartOptions.js │ │ │ └── ChartTypes.js │ ├── databases │ │ ├── DatabaseDeleteModal.js │ │ └── steps │ │ │ ├── DatabaseDetails.js │ │ │ └── DatabaseTypes.js │ ├── emails │ │ └── invite.jsx │ ├── img │ │ ├── Bitbucket.js │ │ ├── Github.js │ │ ├── Gitlab.js │ │ └── Slack.js │ ├── segments │ │ ├── ContactResetModal.js │ │ ├── SegmentDeleteModal.js │ │ ├── SegmentEditModal.js │ │ ├── SegmentSidebar.js │ │ ├── SegmentSwitcher.js │ │ └── steps │ │ │ └── SegmentDataFilter.js │ ├── settings │ │ ├── PricingSection.js │ │ ├── SettingsNav.js │ │ └── SubscriptionSettings.js │ ├── util │ │ ├── Modal.js │ │ └── index.js │ └── workflows │ │ └── nodes │ │ ├── DefaultNode.js │ │ └── util.js ├── config │ ├── permissions.js │ └── pricing.js ├── instrumentation.js ├── lib │ ├── adapters │ │ ├── mysql │ │ │ └── querybuilder.js │ │ └── postgres │ │ │ └── querybuilder.js │ ├── auth.js │ ├── crypto.js │ ├── database.js │ ├── operators.js │ └── stripe │ │ ├── stripe-client.js │ │ ├── stripe.js │ │ └── supabase-admin.js ├── middleware.js ├── providers │ └── Telemetry.js ├── store │ └── index.js └── util │ └── index.js ├── supabase └── migrations │ ├── 20230626041550_create_base_tables.sql │ └── 20230702062454_accounts_and_profiles.sql └── tailwind.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = tab 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_APP_URL=http://localhost:3000 2 | NEXT_PUBLIC_APP_LANDING=/overview 3 | NEXT_PUBLIC_ENV=dev 4 | 5 | # Differentiate between cloud and self-hosted 6 | IS_PLATFORM=true 7 | NEXT_PUBLIC_IS_PLATFORM=true 8 | NEXT_PUBLIC_SIGNUP_CLOSED=false 9 | 10 | # Supabase / database 11 | NEXT_PUBLIC_SUPABASE_URL=https://XYZ.supabase.co 12 | NEXT_PUBLIC_SUPABASE_ANON_KEY=XYZ 13 | SUPABASE_SERVICE_ROLE_KEY=XYZ 14 | SUPABASE_JWT_SECRET=XYZ 15 | 16 | # We encrypt all database connection details before storing them in the database 17 | # This is the key used to encrypt the connection details 18 | CRYPTO_SECRET_KEY=XYZ 19 | CRYPRO_SECRET_IV=XYZ 20 | CRYPTO_ENCRYPTION_METHOD=aes-256-cbc 21 | 22 | # Resend / email 23 | RESEND_API_KEY=re_XYZ 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "eqeqeq": "off", 5 | "quotes": ["error", "double"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .vscode 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/app/api/payments-api"] 2 | path = src/app/api/payments-api 3 | url = https://github.com/supaboard/payments-api.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Logo 4 | 5 | 6 |

Supaboard

7 | 8 |

9 | The dashbord builder for Supabase and Postgres. 10 |
11 | Learn more » 12 |
13 |
14 | Discord 15 | · 16 | Website 17 | · 18 | Issues 19 | · 20 | Discussions 21 |

22 |

23 | 24 | ## Running the app locally 25 | 1. Clone the repo 26 | `https://github.com/supaboard/app.git` 27 | 28 | 2. Install dependencies and start the dev server 29 | `npm i && npm run dev` 30 | 31 | 3. You'll need a Supabase account / Postgress database and [apply](https://supabase.com/docs/reference/cli/supabase-migration-up) the initial migration located in `/supabase`. 32 | We're using a custom schema called `supaboard`, so make sure this accessible from outside connections. In Supabase, go to Settings > API and add `supaboard` to the exposed schemas as shown below. 33 | 34 | ![supabase-schema-permission](https://github.com/supaboard/.github/blob/main/assets/supabase-schema-permission.png) 35 | 36 | 4. Copy or rename the `.env.example` file to `.env` and fill in the environment variables. 37 | **Important:** The variables `IS_PLATFORM` and `NEXT_PUBLIC_IS_PLATFORM` are strongly advised to be set to `false`, otherwise you will run into account limits and other payment issues. The flag is differentiating between our hosted offer and self-hosted instances. 38 | 39 | ## env example 40 | ``` 41 | NEXT_PUBLIC_APP_URL=http://localhost:3000 42 | NEXT_PUBLIC_APP_LANDING=/overview 43 | NEXT_PUBLIC_ENV=dev 44 | 45 | # Differentiate between cloud and self-hosted 46 | IS_PLATFORM=true 47 | NEXT_PUBLIC_IS_PLATFORM=true 48 | NEXT_PUBLIC_SIGNUP_CLOSED=false 49 | 50 | # Supabase / database 51 | NEXT_PUBLIC_SUPABASE_URL=XYZ.supabase.co 52 | NEXT_PUBLIC_SUPABASE_ANON_KEY=XYZ 53 | SUPABASE_SERVICE_ROLE_KEY=XYZ 54 | SUPABASE_JWT_SECRET=XYZ 55 | 56 | # We encrypt all database connection details ebfore storing them in the database 57 | # This is the key used to encrypt the connection details 58 | CRYPTO_SECRET_KEY=XYZ 59 | CRYPRO_SECRET_IV=XYZ 60 | CRYPTO_ENCRYPTION_METHOD=aes-256-cbc 61 | 62 | # Resend / email 63 | RESEND_API_KEY=re_XYZ 64 | 65 | # dev / local tunnel 66 | LOCAL_TUNNEL=https://46e7-2a02-908-4b27-3080-a5fa-9b89-c87d-c9f7.ngrok-free.app/ 67 | ``` 68 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const nextBuildId = require("next-build-id") 2 | 3 | const nextConfig = { 4 | generateBuildId: () => nextBuildId({ dir: __dirname }), 5 | experimental: { 6 | appDir: true, 7 | serverActions: true, 8 | esmExternals: "loose", 9 | instrumentationHook: true 10 | }, 11 | productionBrowserSourceMaps: false, 12 | async redirects() { 13 | return [ 14 | { 15 | source: "/", 16 | destination: "/overview", 17 | permanent: true 18 | } 19 | ] 20 | }, 21 | } 22 | 23 | module.exports = nextConfig 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supaboard", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@headlessui/react": "^1.7.15", 13 | "@heroicons/react": "^2.0.18", 14 | "@highlight-run/next": "^4.0.0", 15 | "@react-email/components": "^0.0.7", 16 | "@stripe/stripe-js": "^1.54.1", 17 | "@supabase/auth-helpers-nextjs": "^0.7.3", 18 | "@supabase/auth-ui-react": "^0.4.2", 19 | "@supabase/auth-ui-shared": "^0.1.6", 20 | "@supabase/supabase-js": "^2.26.0", 21 | "@tippyjs/react": "^4.2.6", 22 | "autoprefixer": "10.4.14", 23 | "common-tags": "^1.8.2", 24 | "crisp-sdk-web": "^1.0.21", 25 | "encoding": "^0.1.13", 26 | "eslint": "8.43.0", 27 | "eslint-config-next": "13.4.7", 28 | "mysql2": "^3.6.0", 29 | "next": "^13.4.8", 30 | "next-build-id": "^3.0.0", 31 | "pg": "^8.11.1", 32 | "postcss": "8.4.24", 33 | "posthog-js": "^1.75.3", 34 | "react": "18.2.0", 35 | "react-apexcharts": "^1.4.0", 36 | "react-dom": "18.2.0", 37 | "react-email": "^1.9.4", 38 | "react-grid-layout": "^1.3.4", 39 | "reactflow": "^11.7.4", 40 | "resend": "^0.17.1", 41 | "rsuite": "^5.37.3", 42 | "server-only": "^0.0.1", 43 | "sonner": "^0.5.0", 44 | "stripe": "^12.13.0", 45 | "tailwindcss": "3.3.2", 46 | "tedious": "^16.1.0", 47 | "vercel-submodules": "^1.0.10", 48 | "zustand": "^4.3.8" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/abf3db6e1933b3d612ad2e1e8b043c02fe42a0dd/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/abf3db6e1933b3d612ad2e1e8b043c02fe42a0dd/public/favicon/favicon.ico -------------------------------------------------------------------------------- /public/favicon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/abf3db6e1933b3d612ad2e1e8b043c02fe42a0dd/public/favicon/favicon.png -------------------------------------------------------------------------------- /public/favicon/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/charts/area-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/abf3db6e1933b3d612ad2e1e8b043c02fe42a0dd/public/img/charts/area-chart.png -------------------------------------------------------------------------------- /public/img/charts/bar-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/abf3db6e1933b3d612ad2e1e8b043c02fe42a0dd/public/img/charts/bar-chart.png -------------------------------------------------------------------------------- /public/img/charts/counter-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/abf3db6e1933b3d612ad2e1e8b043c02fe42a0dd/public/img/charts/counter-chart.png -------------------------------------------------------------------------------- /public/img/charts/pie-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/abf3db6e1933b3d612ad2e1e8b043c02fe42a0dd/public/img/charts/pie-chart.png -------------------------------------------------------------------------------- /public/img/charts/table-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/abf3db6e1933b3d612ad2e1e8b043c02fe42a0dd/public/img/charts/table-chart.png -------------------------------------------------------------------------------- /public/img/databases/mysql.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/databases/planetscale.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/img/databases/supabase.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/img/full-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/abf3db6e1933b3d612ad2e1e8b043c02fe42a0dd/public/img/full-logo.png -------------------------------------------------------------------------------- /public/img/supaboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Group 4 Copy 3 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/app/(app)/contacts/action.js: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { cookies } from "next/headers" 4 | import { createServerActionClient } from "@supabase/auth-helpers-nextjs" 5 | 6 | 7 | export const createContactConnection = async (contactConnection) => { 8 | const cookieStore = cookies() 9 | const account_id = cookieStore.get("account_id").value 10 | const supabase = createServerActionClient({ cookies }, { 11 | options: { 12 | db: { schema: "supaboard" } 13 | } 14 | }) 15 | const { data: { session } } = await supabase.auth.getSession() 16 | if (!session) { 17 | throw new Error("Not authenticated") 18 | } 19 | 20 | contactConnection.account_id = account_id 21 | 22 | try { 23 | const { data, error } = await supabase 24 | .from("contacts") 25 | .insert([ 26 | contactConnection 27 | ]) 28 | .select() 29 | .single() 30 | 31 | if (error) { 32 | console.log(error) 33 | throw Error(error) 34 | } 35 | 36 | return data 37 | } catch (error) { 38 | throw Error(error) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/(app)/contacts/import/page.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect, useTransition } from "react" 4 | import { useRouter } from "next/navigation" 5 | import Link from "next/link" 6 | import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline" 7 | import { ChartDatabase } from "@/components/dashboards/steps/ChartDatabase" 8 | import { SelectContacts } from "@/components/contacts/steps/SelectContacts" 9 | import { createContactConnection } from "../action" 10 | import Loading from "@/components/Loading" 11 | 12 | export default function NewDatabase({ params }) { 13 | const router = useRouter() 14 | let [isPending, startTransition] = useTransition() 15 | const [activeStep, setActiveStep] = useState(1) 16 | const [data, setData] = useState(null) 17 | const [loading, setLoading] = useState(false) 18 | const [database, setDatabase] = useState(null) 19 | const [databases, setDatabases] = useState(null) 20 | const [selectedDataTable, setSelectedDataTable] = useState(null) 21 | const [attributes, setAttributes] = useState(null) 22 | 23 | 24 | useEffect(() => { 25 | const getDatabases = async () => { 26 | setLoading(true) 27 | const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/databases`) 28 | if (!res.ok) { 29 | throw new Error("Failed to fetch data") 30 | } 31 | 32 | let data = await res.json() 33 | if (!data || data.length == 0) { 34 | setDatabases([]) 35 | setDatabase(null) 36 | setLoading(false) 37 | return 38 | } 39 | 40 | setDatabases(data) 41 | setDatabase(data[0].uuid) 42 | setLoading(false) 43 | } 44 | 45 | getDatabases() 46 | }, []) 47 | 48 | 49 | useEffect(() => { 50 | if (activeStep === 2) { 51 | const getChartData = async () => { 52 | const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/externaldb/${database}`) 53 | const data = await res.json() 54 | setData(data) 55 | setSelectedDataTable(data[0].name) 56 | } 57 | getChartData() 58 | } 59 | }, [activeStep, database]) 60 | 61 | const createAllUsersSegment = async () => { 62 | let contactConnection = { 63 | name: "All Users", 64 | database: database, 65 | table_name: selectedDataTable, 66 | attributes: attributes, 67 | } 68 | 69 | startTransition(() => createContactConnection(contactConnection)) 70 | if (!isPending) { 71 | router.push("/segments") 72 | } 73 | } 74 | 75 | 76 | return ( 77 | <> 78 |
79 |
80 |
1
81 | Choose a database 82 |
83 |
84 |
2
85 | Define user attributes 86 |
87 |
88 | 89 |
90 |
91 | {activeStep == 1 && ( 92 | <> 93 |
94 | {loading && ( 95 | 96 | )} 97 | {!loading && ( 98 | 105 | )} 106 |
107 | 108 | )} 109 | 110 | {activeStep == 2 && ( 111 | <> 112 |
113 | 121 |
122 |
123 |
124 | 133 |
134 |
135 | 144 |
145 |
146 | 147 | )} 148 |
149 |
150 | 151 | ) 152 | } 153 | -------------------------------------------------------------------------------- /src/app/(app)/dashboards/[id]/loading.js: -------------------------------------------------------------------------------- 1 | export default function Dashboard({ params }) { 2 | return ( 3 | <> 4 | ) 5 | } -------------------------------------------------------------------------------- /src/app/(app)/dashboards/actions.js: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { cookies } from "next/headers" 4 | import { createServerActionClient } from "@supabase/auth-helpers-nextjs" 5 | 6 | 7 | export const createDashboard = async (formData) => { 8 | const cookieStore = cookies() 9 | const account_id = cookieStore.get("account_id").value 10 | const supabase = createServerActionClient({ cookies }, { 11 | options: { 12 | db: { schema: "supaboard" } 13 | } 14 | }) 15 | const { data: { session } } = await supabase.auth.getSession() 16 | if (!session) { 17 | throw new Error("Not authenticated") 18 | } 19 | 20 | const dashboard_name = formData.get("dashboard_name") 21 | const timeframe = formData.get("timeframe") 22 | 23 | try { 24 | const { data, error } = await supabase 25 | .from("dashboards") 26 | .insert([ 27 | { 28 | name: dashboard_name, 29 | config: { 30 | timeframe, 31 | }, 32 | is_public: false, 33 | account_id: account_id 34 | }, 35 | ]) 36 | .select() 37 | .single() 38 | 39 | if (error) { 40 | console.log(error) 41 | throw Error(error) 42 | } 43 | 44 | return data 45 | } catch (error) { 46 | throw Error(error) 47 | } 48 | } 49 | 50 | 51 | export const updateDashboard = async (dashboard) => { 52 | const supabase = createServerActionClient({ cookies }, { 53 | options: { 54 | db: { schema: "supaboard" } 55 | } 56 | }) 57 | const { data: { session } } = await supabase.auth.getSession() 58 | if (!session) { 59 | throw new Error("Not authenticated") 60 | } 61 | 62 | try { 63 | const { data, error } = await supabase 64 | .from("dashboards") 65 | .update({ 66 | name: dashboard.name, 67 | config: dashboard.config, 68 | is_public: dashboard.is_public || false 69 | }) 70 | .eq("id", dashboard.id) 71 | .select() 72 | 73 | if (error) { 74 | console.log(error) 75 | throw Error(error) 76 | } 77 | 78 | return data 79 | } catch (error) { 80 | throw Error(error) 81 | } 82 | } 83 | 84 | 85 | export const deleteDahboard = async (dashboard) => { 86 | const supabase = createServerActionClient({ cookies }, { 87 | options: { 88 | db: { schema: "supaboard" } 89 | } 90 | }) 91 | const { data: { session } } = await supabase.auth.getSession() 92 | if (!session) { 93 | throw new Error("Not authenticated") 94 | } 95 | 96 | try { 97 | const { data, error } = await supabase 98 | .from("dashboards") 99 | .delete() 100 | .eq("id", dashboard.id) 101 | .select() 102 | .single() 103 | 104 | if (error) { 105 | console.log(error) 106 | throw Error(error) 107 | } 108 | 109 | return data 110 | } catch (error) { 111 | throw Error(error) 112 | } 113 | } 114 | 115 | 116 | export const addChart = async (dashboard_uuid, dashboard_data) => { 117 | const supabase = createServerActionClient({ cookies }, { 118 | options: { 119 | db: { schema: "supaboard" } 120 | } 121 | }) 122 | const { data: { session } } = await supabase.auth.getSession() 123 | if (!session) { 124 | throw new Error("Not authenticated") 125 | } 126 | 127 | console.log(dashboard_data) 128 | try { 129 | const { data, error } = await supabase 130 | .from("dashboards") 131 | .update({ 132 | config: dashboard_data 133 | }) 134 | .eq("uuid", dashboard_uuid) 135 | .select() 136 | .single() 137 | 138 | if (error) { 139 | console.log(error) 140 | throw Error(error) 141 | } 142 | 143 | return data 144 | } catch (error) { 145 | throw Error(error) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/app/(app)/dashboards/loading.js: -------------------------------------------------------------------------------- 1 | import Loading from "@/components/Loading" 2 | 3 | export default async function LoadingDashboards() { 4 | return ( 5 | <> 6 |
7 |
8 |
9 | 10 |
11 |
12 | 13 | Loading dashboards... 14 | 15 |
16 |
17 |
18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/app/(app)/dashboards/page.js: -------------------------------------------------------------------------------- 1 | import { cookies } from "next/headers" 2 | 3 | import Link from "next/link" 4 | import { formatDate } from "@/components/util" 5 | import { NoDashboards } from "@/components/dashboards/NoDashboards" 6 | 7 | async function getData() { 8 | const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/dashboards`, { 9 | headers: { Cookie: cookies().toString() } 10 | }) 11 | 12 | if (!res.ok) { 13 | throw new Error("Failed to fetch data") 14 | } 15 | 16 | return res.json() 17 | } 18 | 19 | 20 | export default async function Dashboards({params, searchParams}) { 21 | const dashboards = await getData() 22 | 23 | return ( 24 | <> 25 |
26 | {dashboards && dashboards.length > 0 && ( 27 |
28 |
29 | 30 | 31 | 32 | 35 | 38 | 41 | 44 | 45 | 46 | 47 | {dashboards.map((dashboard) => ( 48 | 49 | 50 | 55 | 60 | 72 | 77 | 78 | 79 | ))} 80 | 81 |
33 | Name 34 | 36 | Charts 37 | 39 | Timeframe 40 | 42 | Created 43 |
51 | 52 | {dashboard.name} 53 | 54 | 56 | 57 | {dashboard.config?.charts?.length || "no"} charts 58 | 59 | 61 | 62 | {typeof dashboard.config?.timeframe === "string" && dashboard.config?.timeframe?.replaceAll("_", " ")} 63 | {typeof dashboard.config?.timeframe === "object" && ( 64 | <> 65 | { formatDate(dashboard.config?.timeframe[0])} 66 | - 67 | {formatDate(dashboard.config?.timeframe[1])} 68 | 69 | )} 70 | 71 | 73 | 74 | {formatDate(dashboard.created_at)} 75 | 76 |
82 |
83 |
84 | )} 85 | {(!dashboards || dashboards.length == 0) && ( 86 | 87 | )} 88 |
89 | 90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /src/app/(app)/databases/[id]/loading.js: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import Loading from "@/components/Loading" 4 | import { LockClosedIcon } from "@heroicons/react/24/outline" 5 | 6 | 7 | export default async function LoadingEditDatabase({ params }) { 8 | return ( 9 | <> 10 |
11 |
12 |
13 | 14 |
15 |
16 |

17 | Database connection details 18 |

19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 | Decrypting connection details... 29 | 30 |
31 |
32 |
33 | 34 |
35 | 38 | 44 |
45 |
46 | 49 | 54 |
55 |
56 | 59 | 65 |
66 |
67 | 70 | 76 |
77 |
78 | 81 | 88 |
89 |
90 | 93 | 98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | Cancel 106 |
107 |
108 |
109 | 112 |
113 |
114 | 115 |
116 |
117 |
118 | 119 | ) 120 | } 121 | -------------------------------------------------------------------------------- /src/app/(app)/databases/actions.js: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { cookies } from "next/headers" 4 | import { createServerActionClient } from "@supabase/auth-helpers-nextjs" 5 | import { encrypt } from "@/lib/crypto" 6 | import { redirect } from "next/navigation" 7 | 8 | 9 | export const createDatabase = async (name, databaseType, connection) => { 10 | const cookieStore = cookies() 11 | const account_id = cookieStore.get("account_id").value 12 | const supabase = createServerActionClient({ cookies }, { 13 | options: { 14 | db: { schema: "supaboard" } 15 | } 16 | }) 17 | const { data: { session } } = await supabase.auth.getSession() 18 | if (!session) { 19 | throw new Error("Not authenticated") 20 | } 21 | 22 | try { 23 | const encryptedConnection = encrypt(JSON.stringify(connection)) 24 | const { data, error } = await supabase 25 | .from("databases") 26 | .insert([ 27 | { 28 | name: name, 29 | type: databaseType, 30 | connection: encryptedConnection, 31 | account_id: account_id 32 | } 33 | ]) 34 | .select() 35 | .single() 36 | 37 | if (error) { 38 | console.log(error) 39 | throw Error(error) 40 | } 41 | 42 | return data 43 | } catch (error) { 44 | throw Error(error) 45 | } 46 | } 47 | 48 | 49 | 50 | export const updateDatabase = async (formData) => { 51 | const cookieStore = cookies() 52 | const account_id = cookieStore.get("account_id").value 53 | const supabase = createServerActionClient({ cookies }, { 54 | options: { 55 | db: { schema: "supaboard" } 56 | } 57 | }) 58 | const { data: { session } } = await supabase.auth.getSession() 59 | if (!session) { 60 | throw new Error("Not authenticated") 61 | } 62 | 63 | const name = formData.get("name") 64 | const host = formData.get("host") 65 | const port = formData.get("port") 66 | const user = formData.get("user") 67 | const password = formData.get("password") 68 | const database = formData.get("database") 69 | const uuid = formData.get("uuid") 70 | 71 | let connection = { 72 | host: host, 73 | port: port, 74 | user: user, 75 | password: password, 76 | database: database, 77 | } 78 | 79 | const encryptedConnection = encrypt(JSON.stringify(connection)) 80 | const { data, error } = await supabase 81 | .from("databases") 82 | .update({ 83 | name: name, 84 | connection: encryptedConnection 85 | }) 86 | .eq("uuid", uuid) 87 | .select() 88 | .single() 89 | 90 | if (error) { 91 | console.log(error) 92 | throw Error(error) 93 | } 94 | 95 | redirect("/databases") 96 | 97 | } 98 | 99 | 100 | export const deleteDatabase = async (database) => { 101 | const supabase = createServerActionClient({ cookies }, { 102 | options: { 103 | db: { schema: "supaboard" } 104 | } 105 | }) 106 | const { data: { session } } = await supabase.auth.getSession() 107 | if (!session) { 108 | throw new Error("Not authenticated") 109 | } 110 | 111 | try { 112 | const { data, error } = await supabase 113 | .from("databases") 114 | .delete() 115 | .eq("id", database.id) 116 | .select() 117 | .single() 118 | 119 | if (error) { 120 | console.log(error) 121 | throw Error(error) 122 | } 123 | 124 | return data 125 | } catch (error) { 126 | throw Error(error) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/app/(app)/databases/new/page.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, useState, useTransition } from "react" 4 | import { useRouter } from "next/navigation" 5 | import { DatabaseTypes } from "@/components/databases/steps/DatabaseTypes" 6 | import { DatabaseDetails } from "@/components/databases/steps/DatabaseDetails" 7 | import { createDatabase } from "../actions" 8 | import { can } from "@/lib/auth" 9 | import useStore from "@/store/index" 10 | 11 | export default function NewDatabase({ params }) { 12 | const router = useRouter() 13 | let [isPending, startTransition] = useTransition() 14 | const [activeStep, setActiveStep] = useState(1) 15 | const [databaseType, setDatabaseType] = useState(null) 16 | const [databaseDetails, setDatabaseDetails] = useState(null) 17 | const { showUpgradeModal, setShowUpgradeModal } = useStore() 18 | 19 | useEffect(() => { 20 | const checkCreateAllowed = async () => { 21 | const allowed = await can("create:datasource") 22 | if (!allowed) { 23 | setShowUpgradeModal("create:datasource") 24 | } 25 | } 26 | 27 | checkCreateAllowed() 28 | }, []) 29 | 30 | const createNewDatabase = async () => { 31 | let connection = { 32 | host: databaseDetails.host, 33 | port: databaseDetails.port, 34 | user: databaseDetails.user, 35 | password: databaseDetails.password, 36 | database: databaseDetails.database, 37 | } 38 | 39 | startTransition(() => createDatabase(databaseDetails.name, databaseType, connection)) 40 | if (!isPending) { 41 | router.push("/databases") 42 | } 43 | } 44 | 45 | 46 | return ( 47 | <> 48 |
49 |
50 |
1
51 | Choose database type 52 |
53 |
54 |
2
55 | Add connection details 56 |
57 |
58 | 59 |
60 |
61 | {activeStep == 1 && ( 62 | <> 63 |
64 | 70 |
71 | 72 | )} 73 | 74 | {activeStep == 2 && ( 75 | <> 76 |
77 | 85 |
86 | 87 | )} 88 |
89 |
90 | 91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /src/app/(app)/databases/page.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | import { PlusIcon } from "@heroicons/react/24/outline" 3 | import { cookies } from "next/headers" 4 | import { formatDate } from "@/components/util" 5 | 6 | async function getData() { 7 | const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/databases`, { 8 | headers: { Cookie: cookies().toString() } 9 | }) 10 | if (!res.ok) { 11 | throw new Error("Failed to fetch data") 12 | } 13 | 14 | return res.json() 15 | } 16 | 17 | export default async function Databases() { 18 | const databases = await getData() 19 | 20 | return ( 21 |
22 |
23 | 24 | 27 | 28 |
29 |
30 | {(!databases || databases.length == 0) && ( 31 |
32 |
33 | 34 |
35 | 36 | You don't have any databases yet.
Create a new database to get started. 37 |
38 | 39 | 42 | 43 |
44 | )} 45 | {databases && databases.length > 0 && ( 46 |
47 |
48 | {databases && databases.length > 0 && databases.map((database) => ( 49 |
50 | 51 |
52 |
53 | {database.type} 54 |
55 |
56 |

{database.type}

57 | {database.name} 58 |

59 | Created: {formatDate(database.created_at)} 60 |

61 | 64 |
65 |
66 | 67 |
68 | ))} 69 |
70 |
71 | )} 72 |
73 |
74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /src/app/(app)/layout.js: -------------------------------------------------------------------------------- 1 | import { Toaster } from "sonner" 2 | import "tippy.js/dist/tippy.css" 3 | import "@/app/globals.css" 4 | 5 | import { Sidebar } from "@/components/Sidebar" 6 | import { UserMenu } from "@/components/UserMenu" 7 | 8 | export default async function RootLayout({ children }) { 9 | return ( 10 | <> 11 |
12 | 13 |
14 | 15 |
16 |
17 | {children} 18 |
19 |
20 |
21 |
22 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/app/(app)/segments/[id]/page.js: -------------------------------------------------------------------------------- 1 | import { PlusIcon } from "@heroicons/react/24/outline" 2 | import Link from "next/link" 3 | import { cookies } from "next/headers" 4 | 5 | 6 | async function getData(id) { 7 | const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/segments/${id}`, { 8 | headers: { Cookie: cookies().toString() } 9 | }) 10 | if (!res.ok) { 11 | throw new Error("Failed to fetch data") 12 | } 13 | 14 | return res.json() 15 | } 16 | 17 | 18 | export default async function Segments({ params }) { 19 | const { id } = params 20 | const data = await getData(id) 21 | 22 | return ( 23 |
24 | {data.contacts && data.contacts.length > 0 && ( 25 |
26 |
27 |
28 | 29 | 30 | 31 | {data.attributes.map((attribute) => ( 32 | 35 | ))} 36 | 37 | 38 | 39 | {data.contacts.map((person) => ( 40 | 41 | {data.attributes.map((attribute) => ( 42 | 52 | ))} 53 | 54 | ))} 55 | 56 |
33 | {attribute.identifier} 34 |
43 | {person[attribute?.name] && typeof person[attribute?.name] === "object" && ( 44 | <> 45 | {JSON.stringify(person[attribute?.name])} 46 | 47 | )} 48 | {person[attribute?.name] && typeof person[attribute?.name] !== "object" && ( 49 | {person[attribute?.name] || "—"} 50 | )} 51 |
57 |
58 |
59 |
60 | )} 61 |
62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /src/app/(app)/segments/actions.js: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { cookies } from "next/headers" 4 | import { createServerActionClient } from "@supabase/auth-helpers-nextjs" 5 | 6 | 7 | export const createSegment = async (segmentData) => { 8 | const cookieStore = cookies() 9 | const account_id = cookieStore.get("account_id").value 10 | const supabase = createServerActionClient({ cookies }, { 11 | options: { 12 | db: { schema: "supaboard" } 13 | } 14 | }) 15 | const { data: { session } } = await supabase.auth.getSession() 16 | if (!session) { 17 | throw new Error("Not authenticated") 18 | } 19 | 20 | segmentData.account_id = account_id 21 | 22 | try { 23 | const { data, error } = await supabase 24 | .from("segments") 25 | .insert([{ 26 | name: segmentData.name, 27 | config: segmentData.config, 28 | account_id: segmentData.account_id, 29 | database: segmentData.database, 30 | }]) 31 | .select() 32 | .single() 33 | 34 | if (error) { 35 | console.log(error) 36 | throw Error(error) 37 | } 38 | 39 | return data 40 | } catch (error) { 41 | throw Error(error) 42 | } 43 | } 44 | 45 | 46 | export const updateSegment = async (formData) => { 47 | const cookieStore = cookies() 48 | const account_id = cookieStore.get("account_id").value 49 | const supabase = createServerActionClient({ cookies }, { 50 | options: { 51 | db: { schema: "supaboard" } 52 | } 53 | }) 54 | const { data: { session } } = await supabase.auth.getSession() 55 | if (!session) { 56 | throw new Error("Not authenticated") 57 | } 58 | 59 | 60 | try { 61 | const { data, error } = await supabase 62 | .from("segments") 63 | .update([ 64 | { 65 | name: formData.name, 66 | config: formData.config, 67 | } 68 | ]) 69 | .eq("uuid", formData.uuid) 70 | .select() 71 | .single() 72 | 73 | if (error) { 74 | console.log(error) 75 | throw Error(error) 76 | } 77 | 78 | return data 79 | } catch (error) { 80 | throw Error(error) 81 | } 82 | } 83 | 84 | 85 | export const deleteSegment = async (segment) => { 86 | const supabase = createServerActionClient({ cookies }, { 87 | options: { 88 | db: { schema: "supaboard" } 89 | } 90 | }) 91 | const { data: { session } } = await supabase.auth.getSession() 92 | if (!session) { 93 | throw new Error("Not authenticated") 94 | } 95 | 96 | try { 97 | const { data, error } = await supabase 98 | .from("segments") 99 | .delete() 100 | .eq("id", segment.id) 101 | .select() 102 | .single() 103 | 104 | if (error) { 105 | console.log(error) 106 | throw Error(error) 107 | } 108 | 109 | return data 110 | } catch (error) { 111 | throw Error(error) 112 | } 113 | } 114 | 115 | export const deleteContacts = async (segment) => { 116 | const cookieStore = cookies() 117 | const account_id = cookieStore.get("account_id").value 118 | const supabase = createServerActionClient({ cookies }, { 119 | options: { 120 | db: { schema: "supaboard" } 121 | } 122 | }) 123 | 124 | const { data: { session } } = await supabase.auth.getSession() 125 | if (!session) { 126 | throw new Error("Not authenticated") 127 | } 128 | 129 | try { 130 | const { data, error } = await supabase 131 | .from("contacts") 132 | .delete() 133 | .eq("account_id", account_id) 134 | .select() 135 | .single() 136 | 137 | if (error) { 138 | console.log(error) 139 | throw Error(error) 140 | } 141 | 142 | return data 143 | } catch (error) { 144 | throw Error(error) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/app/(app)/segments/new/layout.js: -------------------------------------------------------------------------------- 1 | export default async function SegmentsLayout({ children }) { 2 | return ( 3 |
4 | {children} 5 |
6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /src/app/(app)/segments/page.js: -------------------------------------------------------------------------------- 1 | import { PlusIcon } from "@heroicons/react/24/outline" 2 | import Link from "next/link" 3 | import { cookies } from "next/headers" 4 | 5 | 6 | async function getData() { 7 | const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/contacts`, { 8 | headers: { Cookie: cookies().toString() } 9 | }) 10 | if (!res.ok) { 11 | throw new Error("Failed to fetch data") 12 | } 13 | 14 | return res.json() 15 | } 16 | 17 | 18 | export default async function Segments() { 19 | const data = await getData() 20 | const cookieStore = cookies() 21 | 22 | return ( 23 |
24 | {(!data.contacts || data.contacts.length === 0) && ( 25 |
26 |
27 | 28 |
29 | 30 | You don't have any users yet.
Import contacts to get started. 31 |
32 | 33 | 36 | 37 |
38 | )} 39 | {data.contacts && data.contacts.length > 0 && ( 40 |
41 |
42 |
43 | 44 | 45 | 46 | {data.attributes.map((attribute) => ( 47 | 50 | ))} 51 | 52 | 53 | 54 | {data.contacts.map((person) => ( 55 | 56 | {data.attributes.map((attribute) => ( 57 | 67 | ))} 68 | 69 | ))} 70 | 71 |
48 | {attribute.identifier} 49 |
58 | {person[attribute?.name] && typeof person[attribute?.name] === "object" && ( 59 | <> 60 | {JSON.stringify(person[attribute?.name])} 61 | 62 | )} 63 | {person[attribute?.name] && typeof person[attribute?.name] !== "object" && ( 64 | {person[attribute?.name] || "—"} 65 | )} 66 |
72 |
73 |
74 |
75 | )} 76 |
77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /src/app/(app)/settings/billing/page.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect } from "react" 4 | import { createClientComponentClient } from "@supabase/auth-helpers-nextjs" 5 | import PricingSection from "@/components/settings/PricingSection" 6 | import SubscriptionSettings from "@/components/settings/SubscriptionSettings" 7 | import Loading from "@/components/Loading" 8 | 9 | 10 | export default function Billing() { 11 | const [user, setUser] = useState(null) 12 | const [activeSubscription, setActiveSubscription] = useState(null) 13 | const [loading, setLoading] = useState(true) 14 | const [activeAccount, setActiveAccount] = useState(null) 15 | const supabase = createClientComponentClient({ 16 | options: { 17 | db: { schema: "supaboard" } 18 | } 19 | }) 20 | 21 | useEffect(() => { 22 | const getAccounts = async () => { 23 | setActiveAccount(document.cookie.split("; ").find(row => row.startsWith("account_id")).split("=")[1]) 24 | const { data: { session } } = await supabase.auth.getSession() 25 | setUser(session.user) 26 | } 27 | 28 | getAccounts() 29 | }, []) 30 | 31 | 32 | useEffect(() => { 33 | if (!activeAccount) return 34 | 35 | const getAccountBillingStatus = async () => { 36 | const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/payments-api/status`) 37 | const data = await res.json() 38 | setActiveSubscription(data.subscription) 39 | setLoading(false) 40 | } 41 | 42 | getAccountBillingStatus() 43 | }, [activeAccount]) 44 | 45 | return ( 46 | 47 |
48 | {loading && } 49 | {!loading && ( 50 | <> 51 | 52 | {!activeSubscription && } 53 | {activeSubscription && ( 54 | 55 | )} 56 | 57 | )} 58 |
59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/app/(app)/settings/layout.js: -------------------------------------------------------------------------------- 1 | import { SettingsNav } from "@/components/settings/SettingsNav" 2 | 3 | export default async function SettingsLayout({ children }) { 4 | 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | {children} 12 |
13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/app/(app)/settings/page.js: -------------------------------------------------------------------------------- 1 | import { cookies } from "next/headers" 2 | import { createServerComponentClient } from "@supabase/auth-helpers-nextjs" 3 | 4 | 5 | // async function getData() { 6 | // const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/settings`) 7 | // if (!res.ok) { 8 | // throw new Error('Failed to fetch data') 9 | // } 10 | 11 | // return res.json() 12 | // } 13 | 14 | export default async function Settings() { 15 | const supabase = createServerComponentClient({ cookies }) 16 | const { data: { session } } = await supabase.auth.getSession() 17 | 18 | // const data = await getData() 19 | 20 | return ( 21 | <> 22 |

23 | Manage your personal account information 24 |

25 |
26 |
27 | 28 | Email 29 | 30 |
31 |
32 | 33 |
34 |
35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/app/(app)/settings/workspace/[account_id]/actions.js: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { cookies } from "next/headers" 4 | import { redirect } from "next/navigation" 5 | import { createServerActionClient } from "@supabase/auth-helpers-nextjs" 6 | import { Resend } from "resend" 7 | import { InvitationEmail } from "@/components/emails/invite" 8 | 9 | 10 | export const updateMembers = async (memberData) => { 11 | const cookieStore = cookies() 12 | const account_id = cookieStore.get("account_id").value 13 | const supabase = createServerActionClient({ cookies }, { 14 | options: { 15 | db: { schema: "supaboard" } 16 | } 17 | }) 18 | const { data: { session } } = await supabase.auth.getSession() 19 | if (!session) { 20 | throw new Error("Not authenticated") 21 | } 22 | 23 | let { email, invited_by_user_id, role, send_email } = memberData 24 | 25 | try { 26 | const query = await supabase.from("email_invitations").insert({ 27 | email: email, 28 | account_id: account_id, 29 | invited_by_user_id: invited_by_user_id, 30 | role: role 31 | }) 32 | 33 | if (send_email) { 34 | const { data: teamQuery, error } = await supabase 35 | .from("accounts") 36 | .select("team_name") 37 | .eq("id", account_id) 38 | .single() 39 | 40 | const res = await sendEmail(email, account_id, teamQuery.team_name, role) 41 | if (!res.id) { 42 | throw new Error("Failed to send email") 43 | } 44 | } 45 | 46 | return true 47 | } catch (error) { 48 | console.log(error) 49 | throw Error(error) 50 | } 51 | } 52 | 53 | 54 | export const sendEmail = async (email, account_id, team_name, role) => { 55 | const resend = new Resend(process.env.RESEND_API_KEY) 56 | const res = await resend.emails.send({ 57 | from: "invite@supaboard.co", 58 | to: email, 59 | subject: "👋 You're invited to join your team on Supaboard", 60 | react: , 61 | }) 62 | 63 | return res 64 | } 65 | 66 | export const updateWorkspace = async (wokspace_id, formData) => { 67 | const supabase = createServerActionClient({ cookies }, { 68 | options: { 69 | db: { schema: "supaboard" } 70 | } 71 | }) 72 | const { data: { session } } = await supabase.auth.getSession() 73 | if (!session) { 74 | throw new Error("Not authenticated") 75 | } 76 | 77 | if (!wokspace_id) { 78 | throw new Error("No workspace id provided") 79 | } 80 | 81 | const team_name = formData.get("team_name") 82 | 83 | const { data, error } = await supabase 84 | .from("accounts") 85 | .update([ 86 | { team_name: team_name } 87 | ]) 88 | .eq("id", wokspace_id) 89 | .select() 90 | .single() 91 | 92 | if (error) { 93 | console.log(error) 94 | throw Error(error) 95 | } 96 | 97 | return data 98 | } 99 | 100 | 101 | export const deleteWorkspace = async (wokspace_id) => { 102 | const supabase = createServerActionClient({ cookies }, { 103 | options: { 104 | db: { schema: "supaboard" } 105 | } 106 | }) 107 | const { data: { session } } = await supabase.auth.getSession() 108 | if (!session) { 109 | throw new Error("Not authenticated") 110 | } 111 | 112 | if (!wokspace_id) { 113 | throw new Error("No workspace id provided") 114 | } 115 | 116 | const { data: dahboardsData, error: dahboardsError } = await supabase 117 | .from("dashboards") 118 | .delete() 119 | .eq("account_id", wokspace_id) 120 | 121 | const { data: accountUserData, error: accountUserError } = await supabase 122 | .from("account_user") 123 | .delete() 124 | .eq("account_id", wokspace_id) 125 | 126 | const { data: accountsData, error: accountsError } = await supabase 127 | .from("accounts") 128 | .delete() 129 | .eq("id", wokspace_id) 130 | 131 | if (accountUserError || accountsError || dahboardsError) { 132 | throw Error(accountUserError || accountsError || dahboardsError) 133 | } 134 | 135 | redirect("/settings/workspace") 136 | } 137 | -------------------------------------------------------------------------------- /src/app/(app)/settings/workspace/new/actions.js: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { cookies } from "next/headers" 4 | import { createServerActionClient } from "@supabase/auth-helpers-nextjs" 5 | import { redirect } from "next/navigation" 6 | 7 | 8 | export const createWorkspace = async (formData) => { 9 | const supabase = createServerActionClient({ cookies }, { 10 | options: { 11 | db: { schema: "supaboard" } 12 | } 13 | }) 14 | const { data: { session } } = await supabase.auth.getSession() 15 | if (!session) { 16 | throw new Error("Not authenticated") 17 | } 18 | 19 | const teamName = formData.get("name") 20 | 21 | const { data, error } = await supabase 22 | .from("accounts") 23 | .insert({ 24 | team_name: teamName 25 | }) 26 | .select() 27 | 28 | if (error) { 29 | console.log(error) 30 | throw Error(error) 31 | } 32 | 33 | redirect("/settings/workspace") 34 | } 35 | -------------------------------------------------------------------------------- /src/app/(app)/settings/workspace/new/page.js: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { redirect } from "next/navigation" 4 | import { createWorkspace } from "./actions" 5 | 6 | export default async function NewWorkspace() { 7 | 8 | return ( 9 | <> 10 |

11 | Create a new workspace 12 |

13 | 14 |
15 |
16 | 17 | Workspace name 18 | 19 |
20 |
21 |
22 | 23 |
24 | 30 |
31 |
32 |
33 |
34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/app/(app)/setup/page.js: -------------------------------------------------------------------------------- 1 | 2 | async function getData() { 3 | const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/setup/state`) 4 | if (!res.ok) { 5 | throw new Error("Failed to fetch data") 6 | } 7 | 8 | return res.json() 9 | } 10 | 11 | export default async function Setup() { 12 | const data = await getData() 13 | 14 | return ( 15 |
16 | 17 |
18 | ) 19 | } -------------------------------------------------------------------------------- /src/app/(app)/setup/state/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { createClient } from "@supabase/supabase-js" 3 | 4 | 5 | export async function GET(req) { 6 | return NextResponse.json({ null: [] }) 7 | } 8 | -------------------------------------------------------------------------------- /src/app/(app)/thank-you/page.js: -------------------------------------------------------------------------------- 1 | import Logo from "@/components/brand/Logo" 2 | 3 | export default async function ThankYou() { 4 | return ( 5 |
6 |
7 | 8 |
9 |

Thank you!

10 |

11 | You're awesome. Thank you very much for signing up for Supaboard! If 12 | you need help with anything, don't hesitate to contact us. 13 |

14 |
15 | 16 | Go Home 17 | 18 |
19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/app/(app)/workflows/page.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import ReactFlow, { ReactFlowProvider, Background, Controls, addEdge, applyEdgeChanges, applyNodeChanges } from "reactflow" 4 | import "reactflow/dist/style.css" 5 | import useStore from "@/store/index" 6 | import DefaultNode from "@/components/workflows/nodes/DefaultNode" 7 | import { useEffect, useState } from "react" 8 | 9 | 10 | const nodeTypes = { 11 | defaultNode: DefaultNode, 12 | } 13 | 14 | export default function Workflows() { 15 | const { nodes, setNodes, edges, setEdges } = useStore() 16 | const [flowInstance, setFlowInstance] = useState(null) 17 | 18 | const initFlow = async () => { 19 | let startingNode = { 20 | id: "start", 21 | type: "defaultNode", 22 | position: { x: 0, y: 0 }, 23 | data: { id: "start", label: "This is the start of a conversation" }, 24 | style: { 25 | width: 300, 26 | height: 200, 27 | }, 28 | draggable: true, 29 | } 30 | 31 | setNodes([ 32 | startingNode 33 | ]) 34 | setEdges([]) 35 | } 36 | 37 | useEffect(() => { 38 | if (nodes?.length == 0) { 39 | initFlow() 40 | } 41 | }, [nodes]) 42 | 43 | const onNodesChange = async (changes) => { 44 | let newNodes = applyNodeChanges(changes, nodes) 45 | setNodes(newNodes) 46 | } 47 | 48 | const onEdgesChange = async (changes) => { 49 | let newEdges = applyEdgeChanges(changes, edges) 50 | setEdges(newEdges) 51 | } 52 | 53 | const onConnect = async (connection) => { 54 | let newEdges = addEdge(connection, edges) 55 | setEdges(newEdges) 56 | } 57 | 58 | return ( 59 |
60 | 61 | 72 | 73 | 74 | 75 | 76 |
77 | ) 78 | } -------------------------------------------------------------------------------- /src/app/(auth)/auth/callback/route.js: -------------------------------------------------------------------------------- 1 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 2 | import { cookies } from "next/headers" 3 | import { NextResponse } from "next/server" 4 | 5 | export async function GET(request) { 6 | const requestUrl = new URL(request.url) 7 | const code = requestUrl.searchParams.get("code") 8 | 9 | if (code) { 10 | const supabase = createRouteHandlerClient({ cookies }) 11 | await supabase.auth.exchangeCodeForSession(code) 12 | } 13 | 14 | // URL to redirect to after sign in process completes 15 | return NextResponse.redirect(requestUrl.origin, { status: 302 }) 16 | } 17 | -------------------------------------------------------------------------------- /src/app/(auth)/auth/signout/route.js: -------------------------------------------------------------------------------- 1 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 2 | import { cookies } from "next/headers" 3 | import { NextResponse } from "next/server" 4 | 5 | export async function POST(req) { 6 | const supabase = createRouteHandlerClient({ cookies }) 7 | 8 | // Check if we have a session 9 | const { 10 | data: { session }, 11 | } = await supabase.auth.getSession() 12 | 13 | if (session) { 14 | await supabase.auth.signOut() 15 | } 16 | 17 | return NextResponse.redirect(new URL("/", req.url), { 18 | status: 302, 19 | }) 20 | } -------------------------------------------------------------------------------- /src/app/(auth)/layout.js: -------------------------------------------------------------------------------- 1 | import "@/app/globals.css" 2 | 3 | export const metadata = { 4 | title: "Supaboard — Supabase and Postgres dashboards", 5 | description: "Powerful reporting dashboards on top of Supabase.", 6 | openGraph: { 7 | type: "website", 8 | locale: "en_US", 9 | url: "https://app.supaboard.co", 10 | title: "Supaboard — Supabase and Postgres dashboards", 11 | description: "Powerful reporting dashboards on top of Supabase.", 12 | siteName: "Supaboard", 13 | }, 14 | twitter: { 15 | card: "summary_large_image", 16 | title: "Supaboard — Supabase and Postgres dashboards", 17 | description: "Powerful reporting dashboards on top of Supabase.", 18 | } 19 | } 20 | 21 | export default function AuthLayout({ children }) { 22 | return ( 23 | 24 | 25 |
26 | {children} 27 |
28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/app/(auth)/login/page.js: -------------------------------------------------------------------------------- 1 | import AuthForm from "@/components/auth/AuthForm" 2 | import Logo from "@/components/brand/Logo" 3 | 4 | export default function Login() { 5 | return ( 6 |
7 |
8 |
9 | 10 |
11 |

12 | Sign in to continue. 13 |

14 |
15 |
16 | 17 |
18 |
19 | ) 20 | } -------------------------------------------------------------------------------- /src/app/(auth)/register/page.js: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | import AuthForm from "@/components/auth/AuthForm" 3 | import Logo from "@/components/brand/Logo" 4 | 5 | export default async function Register() { 6 | if (process.env.NEXT_PUBLIC_SIGNUP_CLOSED === "true") { 7 | redirect("/login") 8 | } 9 | 10 | return ( 11 |
12 |
13 |
14 | 15 |
16 |

17 | Create a new account to get started. 18 |

19 |
20 |
21 | 22 |
23 |
24 | ) 25 | } -------------------------------------------------------------------------------- /src/app/(public)/public/layout.js: -------------------------------------------------------------------------------- 1 | import "@/app/globals.css" 2 | 3 | export default function RootLayout({ children }) { 4 | return ( 5 | 6 | 7 |
8 | {children} 9 |
10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/app/api/contacts/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | import { Client } from "pg" 5 | import { checkUserAllowed } from "@/lib/auth" 6 | import { getConnectionDetails } from "@/lib/database" 7 | 8 | 9 | export async function GET(req) { 10 | const cookieStore = cookies() 11 | const account_id = cookieStore.get("account_id").value 12 | 13 | const supabase = createRouteHandlerClient({ cookies }, { 14 | options: { 15 | db: { schema: "supaboard" } 16 | } 17 | }) 18 | 19 | const { data: { session } } = await supabase.auth.getSession() 20 | if (!session) throw new Error("Not authenticated") 21 | 22 | const accountUser = await checkUserAllowed(supabase, session, account_id) 23 | 24 | // Step 1: Get the database connection details and where to find the contacts 25 | let query = supabase.from("contacts").select() 26 | 27 | if (process.env.IS_PLATFORM) { 28 | query = query.eq("account_id", account_id) 29 | } 30 | 31 | const { data: contacts, error } = await query 32 | 33 | if (error) { 34 | console.log(error) 35 | throw new Error("Internal Server Error") 36 | } 37 | 38 | if (!contacts || contacts.length === 0) { 39 | return NextResponse.json([]) 40 | } 41 | 42 | 43 | // Step 2: Fetch the actual contacts from the external database 44 | const columns = contacts[0].attributes.map((attribute) => { 45 | return attribute.name 46 | }) 47 | let result = [] 48 | const connectionDetails = await getConnectionDetails(supabase, account_id, contacts[0].database) 49 | const client = new Client({ 50 | host: connectionDetails.host, 51 | port: parseInt(connectionDetails.port), 52 | user: connectionDetails.user, 53 | password: connectionDetails.password, 54 | database: connectionDetails.database, 55 | }) 56 | await client.connect() 57 | 58 | const externalQuery = await client.query(`SELECT ${columns} FROM ${contacts[0].table_name}`) 59 | result = externalQuery.rows 60 | client.end() 61 | 62 | let returnValue = { 63 | attributes: null, 64 | database: null, 65 | contacts: [] 66 | } 67 | 68 | if (contacts.length > 0) { 69 | returnValue = { 70 | attributes: contacts[0].attributes, 71 | database: contacts[0].database, 72 | contacts: result 73 | } 74 | } 75 | 76 | return NextResponse.json(returnValue) 77 | } 78 | -------------------------------------------------------------------------------- /src/app/api/contacts/segments/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | import { Client } from "pg" 5 | import { checkUserAllowed } from "@/lib/auth" 6 | import { getConnectionDetails } from "@/lib/database" 7 | 8 | 9 | export async function GET(req) { 10 | const cookieStore = cookies() 11 | const account_id = cookieStore.get("account_id").value 12 | 13 | const supabase = createRouteHandlerClient({ cookies }, { 14 | options: { 15 | db: { schema: "supaboard" } 16 | } 17 | }) 18 | 19 | const { data: { session } } = await supabase.auth.getSession() 20 | if (!session) throw new Error("Not authenticated") 21 | 22 | const accountUser = await checkUserAllowed(supabase, session, account_id) 23 | 24 | let query = supabase.from("segments").select() 25 | 26 | if (process.env.IS_PLATFORM) { 27 | query = query.eq("account_id", account_id) 28 | } 29 | 30 | const { data: segments, error } = await query 31 | 32 | if (error) { 33 | console.log(error) 34 | throw new Error("Internal Server Error") 35 | } 36 | 37 | if (!segments || segments.length === 0) { 38 | return NextResponse.json(null) 39 | } 40 | 41 | return NextResponse.json(segments) 42 | } 43 | -------------------------------------------------------------------------------- /src/app/api/counts/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | import { checkUserAllowed } from "@/lib/auth" 5 | 6 | 7 | export async function GET(req) { 8 | const account_id = cookies().get("account_id")?.value 9 | const supabase = createRouteHandlerClient({ cookies }, { 10 | options: { 11 | db: { schema: "supaboard" } 12 | } 13 | }) 14 | 15 | const { data: { session } } = await supabase.auth.getSession() 16 | if (!session) throw new Error("Not authenticated") 17 | 18 | let dbQuery = supabase.from("databases").select("*", { count: "exact"}) 19 | let dashboardQuery = supabase.from("dashboards").select("*", { count: "exact"}) 20 | let segmentQuery = supabase.from("segments").select("*", { count: "exact"}) 21 | 22 | if (process.env.IS_PLATFORM) { 23 | dbQuery = dbQuery.eq("account_id", account_id) 24 | dashboardQuery = dashboardQuery.eq("account_id", account_id) 25 | segmentQuery = segmentQuery.eq("account_id", account_id) 26 | } 27 | 28 | const { data: databases, error: dbError } = await dbQuery 29 | const { data: dashboards, error: dahbordError } = await dashboardQuery 30 | const { data: segments, error: segmentError } = await segmentQuery 31 | 32 | if (dbError || dahbordError || segmentError) { 33 | console.log(dbError, dahbordError, segmentError) 34 | throw new Error("Failed to fetch data") 35 | } 36 | 37 | return NextResponse.json({ 38 | databases: databases?.length || 0, 39 | dashboards: dashboards?.length || 0, 40 | segments: segments?.length || 0, 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /src/app/api/dashboards/[uuid]/charts/[chart_id]/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | 4 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 5 | 6 | 7 | export async function PUT(req, { params }) { 8 | const uuid = params.uuid 9 | const chart_id = params.chart_id 10 | const data = await req.json() 11 | let newConfig 12 | 13 | const supabase = createRouteHandlerClient({ cookies }, { 14 | options: { 15 | db: { schema: "supaboard" } 16 | } 17 | }) 18 | 19 | if (data.duplicate) { 20 | const chart = data.dashboard.config.charts.find((chart) => parseInt(chart.id) == parseInt(chart_id)) 21 | const newChart = { ...chart, id: new Date().getTime() } 22 | newConfig = { ...data.dashboard.config, charts: [...data.dashboard.config.charts, newChart] } 23 | } else { 24 | const charts = data.dashboard.config.charts.filter((chart) => parseInt(chart.id) != parseInt(chart_id)) 25 | newConfig = { ...data.dashboard.config, charts } 26 | } 27 | 28 | const { data: dashboard, error } = await supabase 29 | .from("dashboards") 30 | .update({config: newConfig }) 31 | .eq("uuid", uuid) 32 | .select() 33 | .single() 34 | 35 | if (error) { 36 | console.log(error) 37 | } 38 | 39 | return NextResponse.json(dashboard || []) 40 | } 41 | -------------------------------------------------------------------------------- /src/app/api/dashboards/[uuid]/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | 5 | export async function GET(req, { params }) { 6 | const supabase = createRouteHandlerClient({ cookies }, { 7 | options: { 8 | db: { schema: "supaboard" } 9 | } 10 | }) 11 | 12 | const uuid = params.uuid 13 | 14 | const { data: dashboard, error } = await supabase 15 | .from("dashboards") 16 | .select() 17 | .eq("uuid", uuid) 18 | .single() 19 | 20 | if (error) { 21 | console.log(error) 22 | } 23 | 24 | return NextResponse.json(dashboard || []) 25 | } 26 | 27 | 28 | export async function PUT(req, { params }) { 29 | const supabase = createRouteHandlerClient({ cookies }, { 30 | options: { 31 | db: { schema: "supaboard" } 32 | } 33 | }) 34 | 35 | const uuid = params.uuid 36 | const data = await req.json() 37 | 38 | const { data: dashboard, error } = await supabase 39 | .from("dashboards") 40 | .update({config: data.config }) 41 | .eq("uuid", uuid) 42 | .select() 43 | .single() 44 | 45 | if (error) { 46 | console.log(error) 47 | } 48 | 49 | return NextResponse.json(dashboard || []) 50 | } 51 | -------------------------------------------------------------------------------- /src/app/api/dashboards/data/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | 5 | import { getConnectionDetails } from "@/lib/database" 6 | import { fetchPostgresData } from "@/lib/adapters/postgres/querybuilder" 7 | import { fetchMySQLData } from "@/lib/adapters/mysql/querybuilder" 8 | 9 | 10 | export async function POST(req) { 11 | try { 12 | const data = await req.json() 13 | const cookieStore = cookies() 14 | const account_id = cookieStore.get("account_id").value 15 | 16 | const supabase = createRouteHandlerClient({ cookies }, { 17 | options: { 18 | db: { schema: "supaboard" } 19 | } 20 | }) 21 | 22 | const connectionDetails = await getConnectionDetails(supabase, account_id, data.database) 23 | let result 24 | 25 | if (connectionDetails.type == "postgres" || connectionDetails.type == "supabase") { 26 | result = await fetchPostgresData(data, connectionDetails) 27 | } 28 | 29 | if (connectionDetails.type == "mysql" || connectionDetails.type == "planetscale") { 30 | result = await fetchMySQLData(data, connectionDetails) 31 | } 32 | 33 | return NextResponse.json(result || []) 34 | } catch (error) { 35 | console.log(error) 36 | return NextResponse.json([]) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/api/dashboards/hash/[hash]/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | 4 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 5 | 6 | 7 | export async function GET(req, { params }) { 8 | const hash = params.hash 9 | 10 | const supabase = createRouteHandlerClient({ cookies }, { 11 | options: { 12 | db: { schema: "supaboard" } 13 | } 14 | }) 15 | 16 | const { data: dashboard, error } = await supabase 17 | .from("dashboards") 18 | .select() 19 | .eq("public_hash", hash) 20 | .eq("is_public", true) 21 | .single() 22 | 23 | if (error || !dashboard) { 24 | return new NextResponse("Not found", { status: 404 }) 25 | } 26 | 27 | return NextResponse.json(dashboard) 28 | } 29 | -------------------------------------------------------------------------------- /src/app/api/dashboards/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies, headers } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | import { checkUserAllowed } from "@/lib/auth" 5 | 6 | 7 | export async function GET(req) { 8 | const cookieStore = cookies() 9 | const account_id = cookieStore.get("account_id").value 10 | 11 | const supabase = createRouteHandlerClient({ cookies }, { 12 | options: { 13 | db: { schema: "supaboard" } 14 | } 15 | }) 16 | 17 | const { data: { session } } = await supabase.auth.getSession() 18 | if (!session) throw new Error("Not authenticated") 19 | 20 | const accountUser = await checkUserAllowed(supabase, session, account_id) 21 | 22 | let query = supabase 23 | .from("dashboards") 24 | .select() 25 | 26 | if (process.env.IS_PLATFORM) { 27 | query = query.eq("account_id", account_id) 28 | } 29 | 30 | query.order("created_at", { ascending: false }) 31 | 32 | const { data: dashboards, error } = await query 33 | 34 | 35 | return NextResponse.json(dashboards || []) 36 | } 37 | -------------------------------------------------------------------------------- /src/app/api/databases/[id]/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | import { checkUserAllowed } from "@/lib/auth" 5 | 6 | 7 | export async function GET(req, { params }) { 8 | const id = params.id 9 | const cookieStore = cookies() 10 | const account_id = cookieStore.get("account_id").value 11 | const supabase = createRouteHandlerClient({ cookies }, { 12 | options: { 13 | db: { schema: "supaboard" } 14 | } 15 | }) 16 | 17 | const { data: { session } } = await supabase.auth.getSession() 18 | if (!session) throw new Error("Not authenticated") 19 | 20 | const accountUser = await checkUserAllowed(supabase, session, account_id) 21 | 22 | let query = supabase 23 | .from("databases") 24 | .select() 25 | .eq("uuid", id) 26 | 27 | if (process.env.IS_PLATFORM) { 28 | query = query.eq("account_id", account_id) 29 | } 30 | 31 | query = query.single() 32 | 33 | 34 | const { data: database, error } = await query 35 | 36 | return NextResponse.json(database || []) 37 | } 38 | -------------------------------------------------------------------------------- /src/app/api/databases/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | import { checkUserAllowed } from "@/lib/auth" 5 | 6 | 7 | export async function GET(req) { 8 | const cookieStore = cookies() 9 | const account_id = cookieStore.get("account_id").value 10 | const supabase = createRouteHandlerClient({ cookies }, { 11 | options: { 12 | db: { schema: "supaboard" } 13 | } 14 | }) 15 | 16 | const { data: { session } } = await supabase.auth.getSession() 17 | if (!session) throw new Error("Not authenticated") 18 | 19 | const accountUser = await checkUserAllowed(supabase, session, account_id) 20 | 21 | let query = supabase 22 | .from("databases") 23 | .select() 24 | 25 | if (process.env.IS_PLATFORM) { 26 | query = query.eq("account_id", account_id) 27 | } 28 | 29 | query.order("created_at", { ascending: false }) 30 | 31 | const { data: databases, error } = await query 32 | 33 | return NextResponse.json(databases || []) 34 | } 35 | -------------------------------------------------------------------------------- /src/app/api/externaldb/[id]/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | import { Client } from "pg" 5 | import mysql from "mysql2/promise" 6 | 7 | 8 | import { decrypt } from "@/lib/crypto" 9 | import { checkUserAllowed } from "@/lib/auth" 10 | 11 | export async function GET(req, { params }) { 12 | const id = params.id 13 | const cookieStore = cookies() 14 | const account_id = cookieStore.get("account_id").value 15 | const supabase = createRouteHandlerClient({ cookies }, { 16 | options: { 17 | db: { schema: "supaboard" } 18 | } 19 | }) 20 | 21 | const { data: { session } } = await supabase.auth.getSession() 22 | if (!session) throw new Error("Not authenticated") 23 | 24 | const accountUser = await checkUserAllowed(supabase, session, account_id) 25 | let query = supabase 26 | .from("databases") 27 | .select() 28 | .eq("uuid", id) 29 | 30 | if (process.env.IS_PLATFORM) { 31 | query = query.eq("account_id", account_id) 32 | } 33 | 34 | query = query.single() 35 | const { data: database, error } = await query 36 | 37 | if (error) { 38 | console.log(error) 39 | throw new Error("Failed to fetch database") 40 | } 41 | 42 | const connectionDetails = JSON.parse(decrypt(database.connection)) 43 | const connectionString = `postgresql://${connectionDetails.user}:${connectionDetails.password}@${connectionDetails.host}:${connectionDetails.port}/${connectionDetails.database}` 44 | 45 | let tables = {} 46 | 47 | if (database.type == "postgres" || database.type == "supabase") { 48 | const client = new Client({ 49 | host: connectionDetails.host, 50 | port: parseInt(connectionDetails.port), 51 | user: connectionDetails.user, 52 | password: connectionDetails.password, 53 | database: connectionDetails.database, 54 | }) 55 | await client.connect() 56 | 57 | const res = await client.query("SELECT table_schema, table_name, column_name, data_type FROM information_schema.columns WHERE table_schema = 'public' ORDER BY table_name, column_name") 58 | client.end() 59 | 60 | 61 | 62 | res.rows.forEach((row) => { 63 | if (!tables[row.table_name]) { 64 | tables[row.table_name] = { 65 | name: row.table_name, 66 | columns: [] 67 | } 68 | } 69 | tables[row.table_name].columns.push({ 70 | name: row.column_name, 71 | type: row.data_type 72 | }) 73 | }) 74 | 75 | tables = Object.keys(tables).map((key) => { 76 | return tables[key] 77 | }) 78 | } 79 | 80 | if (database.type == "mysql" || database.type == "planetscale") { 81 | const connection = await mysql.createConnection({ 82 | host: connectionDetails.host, 83 | port: parseInt(connectionDetails.port), 84 | user: connectionDetails.user, 85 | password: connectionDetails.password, 86 | database: connectionDetails.database, 87 | ssl: { 88 | rejectUnauthorized: true, 89 | } 90 | }) 91 | 92 | const [rows, fields] = await connection.execute("SELECT table_schema, table_name, column_name, data_type FROM information_schema.columns WHERE table_schema = ? ORDER BY table_name, column_name", [connectionDetails.database]); 93 | connection.end() 94 | 95 | rows.forEach((row) => { 96 | if (!tables[row.TABLE_NAME]) { 97 | tables[row.TABLE_NAME] = { 98 | name: row.TABLE_NAME, 99 | columns: [] 100 | } 101 | } 102 | tables[row.TABLE_NAME].columns.push({ 103 | name: row.COLUMN_NAME, 104 | type: row.DATA_TYPE 105 | }) 106 | }) 107 | 108 | tables = Object.keys(tables).map((key) => { 109 | return tables[key] 110 | }) 111 | } 112 | 113 | return NextResponse.json(tables || []) 114 | } 115 | -------------------------------------------------------------------------------- /src/app/api/segments/[id]/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | import { Client } from "pg" 5 | 6 | 7 | import { decrypt } from "@/lib/crypto" 8 | import { checkUserAllowed } from "@/lib/auth" 9 | import { operatorMap } from "@/lib/operators" 10 | import { buildQuery } from "@/lib/adapters/postgres/querybuilder" 11 | 12 | export async function GET(req, { params }) { 13 | const id = params.id 14 | const cookieStore = cookies() 15 | const account_id = cookieStore.get("account_id").value 16 | const supabase = createRouteHandlerClient({ cookies }, { 17 | options: { 18 | db: { schema: "supaboard" } 19 | } 20 | }) 21 | 22 | const { data: { session } } = await supabase.auth.getSession() 23 | if (!session) throw new Error("Not authenticated") 24 | 25 | const accountUser = await checkUserAllowed(supabase, session, account_id) 26 | 27 | // Get segment 28 | let query = supabase.from("segments").select().eq("uuid", id) 29 | if (process.env.IS_PLATFORM) { query = query.eq("account_id", account_id) } 30 | query = query.single() 31 | const { data: segment, error } = await query 32 | 33 | if (error) { 34 | console.log(error) 35 | throw Error(error) 36 | } 37 | 38 | // Get database 39 | let databaseQuery = supabase.from("databases").select().eq("uuid", segment.database) 40 | if (process.env.IS_PLATFORM) { databaseQuery = databaseQuery.eq("account_id", account_id) } 41 | query = databaseQuery.single() 42 | const { data: database, error: dbError } = await databaseQuery 43 | 44 | if (error || dbError) { 45 | console.log(error) 46 | console.log(dbError) 47 | throw Error(error || dbError) 48 | } 49 | 50 | 51 | const connectionDetails = JSON.parse(decrypt(database.connection)) 52 | const client = new Client({ 53 | host: connectionDetails.host, 54 | port: parseInt(connectionDetails.port), 55 | user: connectionDetails.user, 56 | password: connectionDetails.password, 57 | database: connectionDetails.database, 58 | }) 59 | await client.connect() 60 | 61 | let segmentQuery 62 | if (segment.config.filterType == "simple") { 63 | segmentQuery = await buildQuery(segment.config) 64 | } else { 65 | segmentQuery = segment.config.filters.query 66 | } 67 | 68 | 69 | const res = await client.query(segmentQuery) 70 | client.end() 71 | 72 | let attributes = res.fields.map((field) => { 73 | return { 74 | name: field.name, 75 | identifier: field.name, 76 | type: field.dataTypeID 77 | } 78 | }) 79 | 80 | return NextResponse.json({ 81 | segment, 82 | attributes, 83 | contacts: res.rows 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /src/app/api/segments/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | import { checkUserAllowed } from "@/lib/auth" 5 | 6 | export async function GET(req, { params }) { 7 | const cookieStore = cookies() 8 | const account_id = cookieStore.get("account_id").value 9 | const supabase = createRouteHandlerClient({ cookies }, { 10 | options: { 11 | db: { schema: "supaboard" } 12 | } 13 | }) 14 | 15 | const { data: { session } } = await supabase.auth.getSession() 16 | if (!session) throw new Error("Not authenticated") 17 | 18 | const accountUser = await checkUserAllowed(supabase, session, account_id) 19 | 20 | let query = supabase 21 | .from("segments") 22 | .select() 23 | 24 | if (process.env.IS_PLATFORM) { 25 | query = query.eq("account_id", account_id) 26 | } 27 | 28 | query.order("created_at", { ascending: false }) 29 | 30 | const { data: segments, error } = await query 31 | 32 | if (error) { 33 | console.log(error) 34 | throw Error(error) 35 | } 36 | 37 | return NextResponse.json(segments) 38 | } 39 | -------------------------------------------------------------------------------- /src/app/api/settings/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | import { checkUserAllowed } from "@/lib/auth" 5 | 6 | export async function GET(req, { params }) { 7 | const cookieStore = cookies() 8 | const account_id = cookieStore.get("account_id").value 9 | const supabase = createRouteHandlerClient({ cookies }, { 10 | options: { 11 | db: { schema: "supaboard" } 12 | } 13 | }) 14 | 15 | const { data: { session } } = await supabase.auth.getSession() 16 | if (!session) throw new Error("Not authenticated") 17 | 18 | const accountUser = await checkUserAllowed(supabase, session, account_id) 19 | let query = supabase 20 | .from("accounts") 21 | .select() 22 | 23 | if (process.env.IS_PLATFORM) { 24 | query = query.eq("account_id", account_id) 25 | } 26 | 27 | query = query.single() 28 | const { data: database, error } = await query 29 | 30 | return NextResponse.json(tables || []) 31 | } 32 | -------------------------------------------------------------------------------- /src/app/api/webhooks/stripe/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { stripe } from "@/lib/stripe/stripe" 3 | import { headers } from "next/headers" 4 | import { upsertProductRecord, upsertPriceRecord, manageSubscriptionStatusChange } from "@/lib/stripe/supabase-admin" 5 | 6 | const relevantEvents = new Set([ 7 | "product.created", 8 | "product.updated", 9 | "price.created", 10 | "price.updated", 11 | "checkout.session.completed", 12 | "customer.subscription.created", 13 | "customer.subscription.updated", 14 | "customer.subscription.deleted" 15 | ]) 16 | 17 | export async function POST(req) { 18 | const rawBody = await req.text() 19 | 20 | const headres = headers() 21 | const signature = headres.get("stripe-signature") 22 | 23 | const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET_LIVE ?? process.env.STRIPE_WEBHOOK_SECRET 24 | let event 25 | 26 | try { 27 | if (!signature || !webhookSecret) return 28 | event = stripe.webhooks.constructEvent(rawBody, signature, webhookSecret) 29 | } catch (err) { 30 | console.log(`❌ Error message: ${err.message}`) 31 | return new NextResponse.json({ error: err.message }, { status: 400 }) 32 | } 33 | 34 | if (relevantEvents.has(event.type)) { 35 | try { 36 | switch (event.type) { 37 | case "product.created": 38 | case "product.updated": 39 | await upsertProductRecord(event.data.object) 40 | break 41 | case "price.created": 42 | case "price.updated": 43 | await upsertPriceRecord(event.data.object) 44 | break 45 | case "customer.subscription.created": 46 | case "customer.subscription.updated": 47 | case "customer.subscription.deleted": 48 | const subscription = event.data.object 49 | await manageSubscriptionStatusChange( 50 | subscription.id, 51 | subscription.customer, 52 | event.type === "customer.subscription.created" 53 | ) 54 | break 55 | case "checkout.session.completed": 56 | const checkoutSession = event.data 57 | .object 58 | if (checkoutSession.mode === "subscription") { 59 | const subscriptionId = checkoutSession.subscription 60 | await manageSubscriptionStatusChange( 61 | subscriptionId, 62 | checkoutSession.customer, 63 | true 64 | ) 65 | } 66 | break 67 | default: 68 | throw new Error("Unhandled relevant event!") 69 | } 70 | } catch (error) { 71 | console.log(error) 72 | return new NextResponse.json({ error: "Webhook handler failed. View logs" }, { status: 400 }) 73 | } 74 | } 75 | 76 | return NextResponse.json({ received: true }) 77 | } 78 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/abf3db6e1933b3d612ad2e1e8b043c02fe42a0dd/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | 12 | body { 13 | color: rgb(var(--foreground-rgb)); 14 | font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 15 | overflow-x: hidden; 16 | } 17 | 18 | .mono { 19 | font-family: 'IBM Plex Mono', monospace; 20 | } 21 | 22 | .bg-gradient { 23 | background-image: linear-gradient(45deg, #E52EE5, #FF7E44); 24 | } 25 | 26 | @layer components { 27 | .btn-default { 28 | @apply text-white px-4 py-2 rounded bg-[#444] hover:bg-gray-800 transition-colors cursor-pointer; 29 | } 30 | 31 | .btn-secondary { 32 | @apply text-[#444] px-4 py-2 rounded border border-[#444] hover:bg-gray-200 transition-colors cursor-pointer; 33 | } 34 | 35 | .btn-empty { 36 | @apply text-black px-4 py-4 rounded border border-gray-200 bg-white hover:bg-gray-200 transition-colors cursor-pointer; 37 | } 38 | 39 | label { 40 | @apply text-gray-500 block uppercase text-[13px] mb-1; 41 | } 42 | 43 | .form-input { 44 | @apply block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-gray-500 focus:border-gray-500 sm:text-sm; 45 | } 46 | } 47 | 48 | .rs-picker-toggle { 49 | @apply !text-dark !border-dark !shadow-none; 50 | } 51 | 52 | .rs-picker-has-value .rs-btn .rs-picker-toggle-value, .rs-picker-has-value .rs-picker-toggle .rs-picker-toggle-value { 53 | @apply !text-dark; 54 | } 55 | 56 | :root { 57 | --rs-btn-primary-bg: #444 !important; 58 | --rs-btn-primary-text: #fff !important; 59 | --rs-btn-link-text: #444 !important; 60 | --rs-btn-primary-hover-bg: #1f2937 !important; 61 | 62 | --rs-calendar-cell-selected-hover: #31dc8e !important; 63 | --rs-bg-active: #31dc8e !important; 64 | --rs-input-focus-border: #31dc8e !important; 65 | --rs-calendar-cell-selected-hover-bg: #31dc8e !important; 66 | --rs-calendar-range-bg: #c5f5db !important; 67 | 68 | 69 | --rs-listbox-option-hover-bg: #c5f5db !important; 70 | --rs-listbox-option-hover-text: #444 !important; 71 | } 72 | 73 | @keyframes blink { 74 | 0% { opacity: 0; } 75 | 100% { opacity: 1; } 76 | } 77 | 78 | .blink { 79 | animation: blink .55s ease-in alternate infinite; 80 | } 81 | 82 | .react-resizable-handle { 83 | opacity: 0.25; 84 | } 85 | 86 | @media (max-width: 768px) { 87 | .react-grid-layout { 88 | padding: 15px !important; 89 | height: auto !important; 90 | } 91 | 92 | .react-grid-item { 93 | width: 100% !important; 94 | position: relative !important; 95 | transform: none !important; 96 | margin: 15px 0 !important; 97 | height: auto !important; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/app/layout.js: -------------------------------------------------------------------------------- 1 | import "@/app/globals.css" 2 | import { UpgradeModal } from "@/components/UpgradeModal" 3 | import { Telemetry } from "@/providers/Telemetry" 4 | 5 | export const metadata = { 6 | metadataBase: new URL("https://app.supaboard.co"), 7 | title: "Supaboard — Supabase and Postgres dashboards", 8 | description: "Create reporting dashboards on top of Supabase with ease.", 9 | icons: { 10 | icon: "/favicon/favicon.png", 11 | }, 12 | } 13 | 14 | 15 | export default function RootLayout({ children }) { 16 | if (process.env.IS_PLATFORM && process.env.NEXT_PUBLIC_ENV != "dev") { 17 | return ( 18 | 19 | {children} 20 | 21 | 22 | ) 23 | } else { 24 | return ( 25 | 26 | 27 | {children} 28 | 29 | 30 | 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/abf3db6e1933b3d612ad2e1e8b043c02fe42a0dd/src/app/opengraph-image.png -------------------------------------------------------------------------------- /src/components/Loading.js: -------------------------------------------------------------------------------- 1 | const Loading = ({width = 25, height = 25, className}) => { 2 | return ( 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | ) 12 | } 13 | 14 | export default Loading -------------------------------------------------------------------------------- /src/components/Skeleton.js: -------------------------------------------------------------------------------- 1 | const Skeleton = ({width, height , className, darkness = 100}) => { 2 | return ( 3 | 4 | 5 | 6 | ) 7 | } 8 | 9 | export default Skeleton 10 | -------------------------------------------------------------------------------- /src/components/UpgradeModal.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import useStore from "@/store/index" 4 | import { useRouter } from "next/navigation" 5 | import Link from "next/link" 6 | import Modal from "./util/Modal" 7 | 8 | export function UpgradeModal() { 9 | const router = useRouter() 10 | const { showUpgradeModal, setShowUpgradeModal } = useStore() 11 | 12 | const redirectBack = () => { 13 | if (showUpgradeModal == "create:segment") { 14 | router.push("/segments") 15 | } 16 | if (showUpgradeModal == "create:dashboard") { 17 | router.push("/dashboards") 18 | } 19 | if (showUpgradeModal == "create:datasource") { 20 | router.push("/databases") 21 | } 22 | } 23 | 24 | return ( 25 | <> 26 | 27 | { 30 | return false 31 | }} 32 | > 33 |
34 |
35 | Upgrade required 36 |
37 |
38 |

39 | {showUpgradeModal == "create:segment" && ( 40 | <> 41 | You've reached the limit of segments you can create on your current plan. Please upgrade to create more segments. 42 | 43 | )} 44 | {showUpgradeModal == "create:dashboard" && ( 45 | <> 46 | You've reached the limit of dashboards you can create on your current plan. Please upgrade to create more dashboards. 47 | 48 | )} 49 | {showUpgradeModal == "create:datasource" && ( 50 | <> 51 | You've reached the limit of data sources you can create on your current plan. Please upgrade to create more data sources. 52 | 53 | )} 54 |

55 |
56 |
57 |
58 | 68 |
69 |
70 | setShowUpgradeModal(false)} className="btn-default inline-block"> 71 | Upgrade now 72 | 73 |
74 |
75 |
76 |
77 | 78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /src/components/WorkspaceSwitcher.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect } from "react" 4 | import { createClientComponentClient } from "@supabase/auth-helpers-nextjs" 5 | import { useRouter } from "next/navigation" 6 | 7 | import { getUserAccounts } from "@/lib/auth" 8 | import Loading from "@/components/Loading" 9 | import Link from "next/link" 10 | 11 | export const WorkspaceSwitcher = ({ modalState }) => { 12 | const router = useRouter() 13 | const [accounts, setAccounts] = useState(null) 14 | const [activeAccount, setActiveAccount] = useState(null) 15 | 16 | const supabase = createClientComponentClient({ 17 | options: { 18 | db: { schema: "supaboard" } 19 | } 20 | }) 21 | 22 | 23 | const changeTeam = async (team) => { 24 | document.cookie = `account_id=${team.account_id}; path=/;` 25 | 26 | if (modalState) { 27 | modalState(false) 28 | } 29 | router.push("/overview") 30 | router.refresh() 31 | } 32 | 33 | useEffect(() => { 34 | const getAccounts = async () => { 35 | setActiveAccount(document.cookie.split("; ").find(row => row.startsWith("account_id")).split("=")[1]) 36 | const { data: { session } } = await supabase.auth.getSession() 37 | let response = await getUserAccounts(supabase, session.user.id) 38 | setAccounts(response) 39 | } 40 | 41 | getAccounts() 42 | }, []) 43 | 44 | 45 | return ( 46 |
47 |
48 | Change workspace 49 |

50 | Switch between the workspaces you're a member of 51 |

52 |
53 |
54 |
55 |
56 | 57 |
58 |
59 | {!accounts && } 60 | {accounts && accounts.map((account, index) => ( 61 | 91 | ))} 92 |
93 |
94 | { 98 | if (modalState) { 99 | modalState(false) 100 | } 101 | }} 102 | > 103 | Create a new workspace 104 | 105 |
106 |
107 |
108 |
109 |
110 |
111 | ) 112 | } 113 | -------------------------------------------------------------------------------- /src/components/auth/AuthForm.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { Auth } from "@supabase/auth-ui-react" 3 | import { ThemeSupa } from "@supabase/auth-ui-shared" 4 | import { createClientComponentClient } from "@supabase/auth-helpers-nextjs" 5 | import { useEffect } from "react" 6 | 7 | export default function AuthForm({ view }) { 8 | const supabase = createClientComponentClient() 9 | 10 | useEffect(() => { 11 | const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => { 12 | if (session) { 13 | window.location.href = `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback` 14 | } 15 | }) 16 | }, []) 17 | 18 | return ( 19 |
20 | 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/components/brand/Logo.js: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | import Link from "next/link" 3 | 4 | const Logo = () => { 5 | return ( 6 | <> 7 | 8 |
9 |
10 | Supaboard 11 |
12 |
13 | Supaboard 14 |
15 |
16 | 17 | 18 | ) 19 | } 20 | 21 | 22 | export default Logo -------------------------------------------------------------------------------- /src/components/contacts/steps/SelectContacts.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, useState } from "react" 4 | import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, TrashIcon } from "@heroicons/react/24/outline" 5 | 6 | export function SelectContacts({ data, selectedDataTable, setSelectedDataTable, selectedDataColumn, setSelectedDataColumn, attributes, setAttributes }) { 7 | const [expanded, setExpanded] = useState([]) 8 | 9 | useEffect(() => { 10 | let attrs = [] 11 | data?.find((table) => table.name == selectedDataTable)?.columns.map((column) => ( 12 | attrs.push({ 13 | identifier: column.name, 14 | name: column.name, 15 | type: column.type, 16 | }) 17 | )) 18 | 19 | setAttributes(attrs) 20 | }, [selectedDataTable]) 21 | 22 | return ( 23 |
24 |

25 | Select where your users are stored and which attributes you want to use for segmentation 26 |

27 |

28 | You'll be able to filter your users by these attributes later on. 29 |

30 | 31 |
32 |
33 | 34 | 44 |
45 | 46 | 47 | 48 |
49 | 50 | 51 | {attributes?.map((attribute) => ( 52 |
53 | { 58 | let attrs = [...attributes] 59 | attrs.find((attr) => attr.name == attribute.name).name = e.target.value 60 | setAttributes(attrs) 61 | }} 62 | /> 63 | 77 |
78 | { 79 | setAttributes(attributes.filter((attr) => attr.name != attribute.name)) 80 | }} /> 81 |
82 |
83 | ) 84 | )} 85 |
86 |
87 |
88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /src/components/dashboards/DashboardDeleteModal.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Modal from "@/components/util/Modal" 4 | import { deleteDahboard } from "@/app/(app)/dashboards/actions" 5 | import { toast } from "sonner" 6 | import { useRouter } from "next/navigation" 7 | 8 | export function DashboardDeleteModal({ dashboard, showModal, setShowModal }) { 9 | const router = useRouter() 10 | 11 | return ( 12 | <> 13 | setShowModal(false)} 16 | > 17 |
18 |
19 | Really detele this dashboard? 20 |
21 |
23 | deleteDahboard(dashboard) 24 | .then(async (res) => { 25 | setShowModal(false) 26 | toast.success("Dashboard deleted!") 27 | router.push("/dashboards") 28 | router.refresh() 29 | }) 30 | .catch((err) => toast.error(err.message)) 31 | } 32 | className="rounded-b-lg bg-white" 33 | > 34 |

35 | Are you sure you want to delete the dashboard "{dashboard?.name}" and all its charts and settings? This action cannot be undone. 36 |

37 |
38 |
setShowModal(false)} 41 | > 42 | Cancel 43 |
44 | 45 |
46 |
47 |
48 |
49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/components/dashboards/DashboardEditModal.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Modal from "@/components/util/Modal" 4 | import { updateDashboard } from "@/app/(app)/dashboards/actions" 5 | import { toast } from "sonner" 6 | import { useRouter } from "next/navigation" 7 | 8 | export function DashboardEditModal({ dashboard, showModal, setShowModal, setUpdateHash }) { 9 | const router = useRouter() 10 | 11 | return ( 12 | <> 13 | setShowModal(false)} 16 | > 17 |
18 |
19 | Edit this dashboard 20 |
21 |
{ 23 | let updates = { 24 | ...dashboard, 25 | name: data.get("name"), 26 | config: { 27 | ...dashboard.config, 28 | timeframe: data.get("timeframe") 29 | } 30 | } 31 | 32 | updateDashboard(updates) 33 | .then(async (res) => { 34 | setShowModal(false) 35 | toast.success("Dashboard saved!") 36 | setUpdateHash(Math.random()) 37 | router.push(`/dashboards/${dashboard.uuid}`) 38 | router.refresh() 39 | }) 40 | .catch((err) => toast.error(err.message)) 41 | } 42 | } 43 | className="rounded-b-lg bg-white" 44 | > 45 |
46 |
47 | 48 | 56 |
57 | 58 | 64 |
65 |
66 |
67 |
68 | 74 | 75 |
76 |
77 |
78 |
79 | 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /src/components/dashboards/DashboardNewModal.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import Modal from "@/components/util/Modal" 5 | import { createDashboard } from "@/app/(app)/dashboards/actions" 6 | import { toast } from "sonner" 7 | import { useRouter } from "next/navigation" 8 | import useStore from "@/store/index" 9 | import { can } from "@/lib/auth" 10 | import Loading from "../Loading" 11 | 12 | 13 | export function DashboardNewModal({ showModal, setShowModal }) { 14 | const router = useRouter() 15 | const { showUpgradeModal, setShowUpgradeModal } = useStore() 16 | const [loading, setLoading] = useState(false) 17 | 18 | const checkCreateAllowed = async (e) => { 19 | e.preventDefault() 20 | setLoading(true) 21 | 22 | const allowed = await can("create:dashboard") 23 | if (!allowed) { 24 | setLoading(false) 25 | setShowModal(false) 26 | setShowUpgradeModal("create:dashboard") 27 | } else { 28 | e.target.form.requestSubmit() 29 | } 30 | } 31 | 32 | return ( 33 | <> 34 | setShowModal(false)} 37 | > 38 |
39 |
40 | Create a dashboard 41 |
42 |
44 | createDashboard(data) 45 | .then((res) => { 46 | setShowModal(false) 47 | toast.success("Dashboard created!") 48 | router.push(`/dashboards/${res.uuid}`) 49 | router.refresh() 50 | }) 51 | .catch((err) => toast.error(err.message)) 52 | } 53 | className="rounded-b-lg bg-white" 54 | > 55 |
56 |
57 | 58 | 59 |
60 |
61 | 62 | 68 |
69 |
70 |
71 | 78 | 93 |
94 |
95 |
96 |
97 | 98 | ) 99 | } 100 | -------------------------------------------------------------------------------- /src/components/dashboards/NoDashboards.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, useState } from "react" 4 | import { PlusIcon } from "@heroicons/react/20/solid" 5 | import { Toaster } from "sonner" 6 | 7 | import { DashboardNewModal } from "@/components/dashboards/DashboardNewModal" 8 | 9 | 10 | export function NoDashboards() { 11 | const [showModal, setShowModal] = useState(false) 12 | 13 | return ( 14 | <> 15 |
16 |
17 | 18 |
19 | 20 | You don't have any dashboards yet.
Create a new dashboard to get started. 21 |
22 | 28 |
29 | 30 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/components/dashboards/charts/AreaChart.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect } from "react" 4 | import dynamic from "next/dynamic" 5 | const Chart = dynamic(() => import("react-apexcharts"), { ssr: false }) 6 | import { ChartHeader } from "./ChartHeader" 7 | import Loading from "@/components/Loading" 8 | import { ChartError } from "./ChartError" 9 | 10 | 11 | export function AreaChart({ dashboard_uuid, chartData, timeframe, is_public }) { 12 | const [chartOptions, setChartOptions] = useState({}) 13 | const [chartSeries, setChartSeries] = useState([]) 14 | const [loading, setLoading] = useState(true) 15 | const [hasError, setHasError] = useState(false) 16 | 17 | useEffect(() => { 18 | if (!chartData) return 19 | if (!dashboard_uuid) return 20 | 21 | const fetchChartData = async () => { 22 | const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/dashboards/data`, { 23 | method: "POST", 24 | headers: { 25 | "Accept": "application/json", 26 | "Content-Type": "application/json" 27 | }, 28 | body: JSON.stringify({ 29 | dashboard_uuid: dashboard_uuid, 30 | ...chartData 31 | }) 32 | }) 33 | 34 | const data = await res.json() 35 | if (!data || data.length === 0 || !res.ok) { 36 | setHasError(true) 37 | } 38 | 39 | let dataCol = chartData.dataCol 40 | let timeCol = chartData.timeCol 41 | 42 | let UsageByDay = data.reduce((acc, item) => { 43 | const date = new Date(item[timeCol]) 44 | const day = date.getDate() 45 | const month = date.getMonth() + 1 46 | const year = date.getFullYear() 47 | const key = `${year}-${month}-${day}` 48 | if (!acc[key]) { 49 | acc[key] = 0 50 | } 51 | acc[key] += 1 52 | return acc 53 | }, {}) 54 | 55 | // Limit data to timeframe 56 | if (timeframe) { 57 | UsageByDay = Object.keys(UsageByDay).reduce((acc, key) => { 58 | const date = new Date(key) 59 | const timestamp = date.getTime() 60 | const timestampFrame = timeframe.map((t) => new Date(t).getTime()) 61 | if (timestamp >= timestampFrame[0] && timestamp <= timestampFrame[1]) { 62 | acc[key] = UsageByDay[key] 63 | } 64 | return acc 65 | } 66 | , {}) 67 | } 68 | 69 | let series = [] 70 | let dataPoints = [] 71 | let days = [] 72 | 73 | Object.keys(UsageByDay).map((key) => { 74 | const date = new Date(key) 75 | days.push(date.toLocaleDateString()) 76 | dataPoints.push(UsageByDay[key]) 77 | series.push({ 78 | data: [UsageByDay[key]], 79 | name: date.toLocaleDateString() 80 | }) 81 | }) 82 | 83 | 84 | setChartOptions({ 85 | chart: { 86 | id: "basic-bar", 87 | toolbar: { 88 | show: false 89 | } 90 | }, 91 | xaxis: { 92 | categories: days 93 | }, 94 | fill: { 95 | colors: ["#31dc8e", "#444"] 96 | }, 97 | stroke: { 98 | colors: ["#31dc8e",] 99 | }, 100 | dataLabels: { 101 | style: { 102 | colors: ["#444"] 103 | } 104 | } 105 | }) 106 | 107 | setChartSeries([ 108 | { 109 | name: "Amount", 110 | data: dataPoints 111 | } 112 | ]) 113 | 114 | setLoading(false) 115 | } 116 | 117 | fetchChartData() 118 | }, [chartData, dashboard_uuid, timeframe]) 119 | 120 | return ( 121 |
122 | 123 |
124 | {loading && ( 125 | 126 | )} 127 | {!loading && !hasError && ( 128 | 135 | )} 136 | {!loading && hasError && ( 137 | 138 | )} 139 |
140 |
141 | ) 142 | } 143 | -------------------------------------------------------------------------------- /src/components/dashboards/charts/ChartError.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { ExclamationTriangleIcon } from "@heroicons/react/24/outline" 3 | 4 | export function ChartError() { 5 | return ( 6 |
7 |
8 | 9 |
Error loading chart
10 |
11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/components/dashboards/charts/ChartErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { ChartError } from "./ChartError" 3 | 4 | 5 | class ChartErrorBoundary extends React.Component { 6 | constructor(props) { 7 | super(props) 8 | this.state = { hasError: false } 9 | } 10 | 11 | static getDerivedStateFromError(error) { 12 | return { hasError: true } 13 | } 14 | 15 | componentDidCatch(error, errorInfo) { 16 | console.log({ error, errorInfo }) 17 | } 18 | 19 | render() { 20 | if (this.state.hasError) { 21 | return ( 22 | 23 | ) 24 | } 25 | 26 | return this.props.children 27 | } 28 | } 29 | 30 | export default ChartErrorBoundary 31 | -------------------------------------------------------------------------------- /src/components/dashboards/charts/ChartHeader.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { EllipsisHorizontalIcon, PencilSquareIcon, Square2StackIcon, TrashIcon } from "@heroicons/react/24/outline" 4 | import { Menu, Transition } from "@headlessui/react" 5 | import { Fragment, useEffect, useRef, useState } from "react" 6 | import { ChevronDownIcon } from "@heroicons/react/20/solid" 7 | import useStore from "@/store/index" 8 | import { ChartEditModal } from "./ChartEditModal" 9 | 10 | export function ChartHeader({ dashboard_uuid, chartData, is_public }) { 11 | const { dashboard, setDashboard } = useStore() 12 | const [showEditModal, setShowEditModal] = useState(false) 13 | 14 | const deleteChart = async () => { 15 | const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/dashboards/${dashboard_uuid}/charts/${chartData.id}`, { 16 | method: "PUT", 17 | body: JSON.stringify({ 18 | dashboard: dashboard 19 | }) 20 | }) 21 | const data = await res.json() 22 | setDashboard(data) 23 | } 24 | 25 | const duplicateChart = async () => { 26 | const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/dashboards/${dashboard_uuid}/charts/${chartData.id}`, { 27 | method: "PUT", 28 | body: JSON.stringify({ 29 | dashboard: dashboard, 30 | duplicate: true 31 | }) 32 | }) 33 | const data = await res.json() 34 | setDashboard(data) 35 | } 36 | 37 | const editChart = () => { 38 | setShowEditModal(true) 39 | } 40 | 41 | return ( 42 |
43 |
44 |
45 | {chartData.name} 46 | {!is_public && ( 47 |
48 | 49 |
50 | 51 | 52 | 53 |
54 | 63 | 64 |
65 | 66 | {({ active }) => ( 67 | 75 | )} 76 | 77 | 78 | {({ active }) => ( 79 | 87 | )} 88 | 89 | 90 | {({ active }) => ( 91 | 99 | )} 100 | 101 |
102 |
103 |
104 |
105 |
106 | )} 107 |
108 |

109 | {chartData.description} 110 |

111 | 112 | 113 | 114 |
115 | ) 116 | } 117 | -------------------------------------------------------------------------------- /src/components/dashboards/charts/CounterChart.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect } from "react" 4 | import { ChartHeader } from "./ChartHeader" 5 | import Loading from "@/components/Loading" 6 | import { ChartError } from "./ChartError" 7 | 8 | 9 | export function CounterChart({ dashboard_uuid, chartData, timeframe, is_public }) { 10 | const [counter, setCounter] = useState(null) 11 | const [loading, setLoading] = useState(true) 12 | const [headerSize, setHeaderSize] = useState(60) 13 | const [hasError, setHasError] = useState(false) 14 | 15 | useEffect(() => { 16 | if (!chartData) return 17 | if (!dashboard_uuid) return 18 | 19 | const fetchChartData = async () => { 20 | const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/dashboards/data`, { 21 | method: "POST", 22 | headers: { 23 | "Accept": "application/json", 24 | "Content-Type": "application/json" 25 | }, 26 | body: JSON.stringify({ 27 | dashboard_uuid: dashboard_uuid, 28 | ...chartData 29 | }) 30 | }) 31 | 32 | const data = await res.json() 33 | if (!data || data.length === 0 || !res.ok) { 34 | setHasError(true) 35 | } 36 | 37 | let timeCol = chartData.timeCol 38 | 39 | // Limit data to timeframe 40 | if (timeframe) { 41 | const start = new Date(timeframe[0]).getTime() 42 | const end = new Date(timeframe[1]).getTime() 43 | const filteredData = data.filter((item) => { 44 | const timestamp = new Date(item[timeCol]).getTime() 45 | return timestamp >= start && timestamp <= end 46 | }) 47 | setCounter(filteredData.length) 48 | } else { 49 | setCounter(data.length) 50 | } 51 | 52 | setLoading(false) 53 | } 54 | 55 | fetchChartData() 56 | }, [chartData, dashboard_uuid, timeframe]) 57 | 58 | useEffect(() => { 59 | const header = document.querySelector(".chart-header") 60 | if (!header) return 61 | setHeaderSize(header.offsetHeight) 62 | }, [headerSize]) 63 | 64 | 65 | return ( 66 |
67 | 68 |
69 |
70 | {loading && !hasError && ( 71 | 72 | )} 73 | {!loading && !hasError && ( 74 | <> 75 | {counter} 76 | 77 | )} 78 | {!loading && hasError && ( 79 | 80 | )} 81 |
82 |
83 |
84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /src/components/dashboards/charts/PieChart.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect } from "react" 4 | import dynamic from "next/dynamic" 5 | const Chart = dynamic(() => import("react-apexcharts"), { ssr: false }) 6 | import { ChartHeader } from "./ChartHeader" 7 | import Loading from "@/components/Loading" 8 | import { ChartError } from "./ChartError" 9 | 10 | 11 | export function PieChart({ dashboard_uuid, chartData, timeframe, is_public }) { 12 | const [chartOptions, setChartOptions] = useState({}) 13 | const [chartSeries, setChartSeries] = useState([]) 14 | const [loading, setLoading] = useState(true) 15 | const [hasError, setHasError] = useState(false) 16 | 17 | useEffect(() => { 18 | if (!chartData) return 19 | if (!dashboard_uuid) return 20 | 21 | const fetchChartData = async () => { 22 | const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/dashboards/data`, { 23 | method: "POST", 24 | headers: { 25 | "Accept": "application/json", 26 | "Content-Type": "application/json" 27 | }, 28 | body: JSON.stringify({ 29 | dashboard_uuid: dashboard_uuid, 30 | ...chartData 31 | }) 32 | }) 33 | 34 | const data = await res.json() 35 | if (!data || data.length === 0 || !res.ok) { 36 | setHasError(true) 37 | } 38 | 39 | let dataCol = chartData.dataCol 40 | let timeCol = chartData.timeCol 41 | 42 | let UsageByDay = data.data.reduce((acc, item) => { 43 | const date = new Date(item[timeCol]) 44 | const day = date.getDate() 45 | const month = date.getMonth() + 1 46 | const year = date.getFullYear() 47 | const key = `${year}-${month}-${day}` 48 | if (!acc[key]) { 49 | acc[key] = 0 50 | } 51 | acc[key] += 1 52 | return acc 53 | }, {}) 54 | 55 | // Limit data to timeframe 56 | if (timeframe) { 57 | UsageByDay = Object.keys(UsageByDay).reduce((acc, key) => { 58 | const date = new Date(key) 59 | const timestamp = date.getTime() 60 | const timestampFrame = timeframe.map((t) => new Date(t).getTime()) 61 | if (timestamp >= timeframe[0] && timestamp <= timeframe[1]) { 62 | acc[key] = UsageByDay[key] 63 | } 64 | return acc 65 | } 66 | , {}) 67 | } 68 | 69 | let series = [] 70 | let labels = [] 71 | 72 | data.map((dataPoint) => { 73 | series.push(dataPoint.source_ids.length) 74 | labels.push(dataPoint.name) 75 | }) 76 | 77 | setChartOptions({ 78 | chart: { 79 | id: "donut", 80 | toolbar: { 81 | show: false 82 | } 83 | }, 84 | labels: labels, 85 | dataLabels: { 86 | style: { 87 | colors: ["#444"] 88 | } 89 | } 90 | }) 91 | 92 | setChartSeries(series) 93 | setLoading(false) 94 | } 95 | 96 | fetchChartData() 97 | }, [chartData, dashboard_uuid, timeframe]) 98 | 99 | return ( 100 |
101 | 102 |
103 | {loading && !hasError && ( 104 | 105 | )} 106 | {!loading && !hasError && ( 107 | 114 | )} 115 | {!loading && hasError && ( 116 | 117 | )} 118 |
119 |
120 | ) 121 | } 122 | -------------------------------------------------------------------------------- /src/components/dashboards/charts/TableChart.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect } from "react" 4 | import { ChartHeader } from "./ChartHeader" 5 | import Loading from "@/components/Loading" 6 | import { ChartError } from "./ChartError" 7 | 8 | export function TableChart({ dashboard_uuid, chartData, is_public }) { 9 | const [data, setData] = useState(null) 10 | const [loading, setLoading] = useState(true) 11 | const [headerSize, setHeaderSize] = useState(60) 12 | const [hasError, setHasError] = useState(false) 13 | 14 | useEffect(() => { 15 | if (!chartData) return 16 | if (!dashboard_uuid) return 17 | 18 | const fetchChartData = async () => { 19 | const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/dashboards/data`, { 20 | method: "POST", 21 | headers: { 22 | "Accept": "application/json", 23 | "Content-Type": "application/json" 24 | }, 25 | body: JSON.stringify({ 26 | dashboard_uuid: dashboard_uuid, 27 | ...chartData 28 | }) 29 | }) 30 | 31 | const data = await res.json() 32 | if (!data || data.length === 0 || !res.ok) { 33 | setHasError(true) 34 | } 35 | 36 | setData(data) 37 | setLoading(false) 38 | } 39 | 40 | fetchChartData() 41 | }, [chartData, dashboard_uuid]) 42 | 43 | useEffect(() => { 44 | const header = document.querySelector(".chart-header") 45 | if (!header) return 46 | setHeaderSize(header.offsetHeight) 47 | }, [headerSize]) 48 | 49 | 50 | return ( 51 |
52 | 53 |
54 |
55 | {loading && !hasError && ( 56 | 57 | )} 58 | {!loading && !hasError && ( 59 |
60 |
61 | 62 | 63 | 64 | {data && data.length > 0 && Object.keys(data[0]).map((key) => ( 65 | 68 | ))} 69 | 70 | 71 | 72 | {data && data.length > 0 && data.slice(0,5).map((row, index) => ( 73 | 74 | {Object.keys(row).map((key) => ( 75 | 78 | ))} 79 | 80 | ))} 81 | 82 |
66 | {key} 67 |
76 | {row[key]} 77 |
83 |
84 |
85 | )} 86 | {!loading && hasError && ( 87 | 88 | )} 89 |
90 |
91 |
92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /src/components/dashboards/steps/ChartDatabase.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ArrowLeftIcon, ArrowRightIcon, ArrowUpRightIcon } from "@heroicons/react/24/outline" 4 | import Link from "next/link" 5 | import { useEffect, useState } from "react" 6 | 7 | export function ChartDatabase({ activeStep, setActiveStep, databases, setDatabase, cancelUrl }) { 8 | 9 | return ( 10 |
11 |
12 | Database connection 13 |
14 | {(!databases || databases.length == 0) && ( 15 |
16 |

17 | You don't have any databases connected to your account. Please connect a database to continue. 18 |

19 | 20 | Connect a database 21 | 22 |
23 | )} 24 | {databases && databases.length > 0 && ( 25 | <> 26 |
27 | 30 | 43 |
44 |
45 |
46 | {cancelUrl && ( 47 | 51 | Cancel 52 | 53 | )} 54 | {!cancelUrl && ( 55 | 64 | )} 65 |
66 |
67 | 76 |
77 |
78 | 79 | )} 80 |
81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /src/components/dashboards/steps/ChartOptions.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ArrowLeftIcon } from "@heroicons/react/24/outline" 4 | 5 | export function ChartOptions({ chartType, options, setOptions, saveChart, activeStep, setActiveStep }) { 6 | return ( 7 |
8 |
9 | Chart options 10 |
11 |
12 |
13 | 16 | { 23 | setOptions({ 24 | ...options, 25 | name: e.target.value 26 | }) 27 | }} 28 | /> 29 |
30 |
31 | 34 |