├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── .env.example ├── .gitignore ├── README.md ├── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── hooks │ └── useFetch.tsx ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── server │ └── token.ts ├── tailwind.config.js └── tsconfig.json ├── datagen ├── .gitignore ├── README.md ├── mockDataGenerator.js ├── package-lock.json ├── package.json └── utils │ └── tinybird.js ├── img ├── app_screenshot.png └── data_flow.png ├── package-lock.json └── tinybird ├── README.md ├── datasources ├── accounts.datasource └── signatures.datasource └── pipes └── ranking_of_top_organizations_creating_signatures.pipe /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 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 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # python 40 | .venv 41 | 42 | # tinybird 43 | .tinyb -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | ### Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ### Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ### Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ### Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ### Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [hi@tinybird.co](mailto:hi@tinybird.co). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ### Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How do I make a contribution? 2 | 3 | We welcome contributions from the community: New features, improvements to the existing ones, minor fixes, quality code related tasks... anything else that raises the bar of the project. 4 | 5 | Here's how to contribute a new Pull Request to the project: 6 | 7 | ### Branching strategy 8 | 9 | - Create a new branch, using these prefixes: `feature/`, `doc/`, `bugfix/`, etc. 10 | - Set `main` as target. 11 | 12 | ### Rules 13 | 14 | Every pull request must comply all of the following: 15 | 16 | - Motivation and description (with technical details). 17 | - Successful functional validation. 18 | - Successful build status. 19 | - No merge conflicts. 20 | - No dependencies conflicts. 21 | - Follow code style guide. 22 | - All tests passing. 23 | - Updated README and CHANGELOG if applies. 24 | - All contributions must follow the [Code of Conduct](CODE_OF_CONDUCT.md). 25 | 26 | If you have any questions, please contact the maintainers. 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tinybird 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Use Case Demo: SaaS dashboards 2 | 3 | This repository contains code for an example SaaS dashboard project. 4 | 5 | If you're building a SaaS product, you'll need to provide users with visibility about their utilisation of the service. This might be showing resource usage, performance metrics, or other data that's relevant to the service you're providing. Whatever the data, users want to see fresh data, not yesterday's, and they don't want to wait for slow metrics and charts. This projects shows how to build a SaaS dashboard that's fast, scalable, and easy to maintain using Tinybird, and how to integrate it with a Next.js frontend. 6 | 7 | Read more about [user-facing SaaS dashboards here ](https://www.tinybird.co/docs/use-cases/saas-dashboards](https://www.tinybird.co/docs/use-cases/user-facing-dashboards)). 8 | 9 | ## Watch the workshop 10 | 11 | Want to watch somebody build this demo live? [Watch the workshop](https://www.tinybird.co/docs/live/kafka-real-time-dashboard). 12 | 13 | ## Deploy it yourself 14 | 15 | [Read the guide](https://www.tinybird.co/docs/guides/tutorials/real-time-dashboard) to deploy this demo application yourself. 16 | 17 | ## License 18 | 19 | This code is available under the MIT license. See the [LICENSE](./LICENSE.txt) file for more details. 20 | 21 | ## Need help? 22 | 23 | - [Tinybird Slack Community](https://www.tinybird.co/community) 24 | - [Tinybird Docs](https://www.tinybird.co/docs) 25 | 26 | ## Authors 27 | 28 | - [Joe Karlsson](https://github.com/joekarlsson) 29 | - [Cameron Archer](https://github.com/tb-peregrine) 30 | - [Lucy Mitchell](https://github.com/ioreka) 31 | - [Julia Vallina](https://github.com/juliavallina) 32 | -------------------------------------------------------------------------------- /app/.env.example: -------------------------------------------------------------------------------- 1 | TINYBIRD_SIGNING_TOKEN="YOUR SIGNING TOKEN" 2 | TINYBIRD_WORKSPACE="YOUR WORKSPACE ID" 3 | NEXT_PUBLIC_TINYBIRD_HOST="YOUR API HOST. E.G. https://api.tinybird.co" -------------------------------------------------------------------------------- /app/.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 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 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # Real-time Signatures Dashboard 2 | 3 | This folder contains all of the code to build the frontend of this demo. 4 | 5 | ![App Screenshot](../img/app_screenshot.png "App Screenshot") 6 | 7 | ## Deploying the app 8 | 9 | This is a NextJS application. Deploy it to the cloud with Vercel. 10 | 11 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Ftinybirdco%2Fsignatures-dashboard%2Ftree%2Fmain%2Fapp&env=NEXT_PUBLIC_TINYBIRD_AUTH_TOKEN,NEXT_PUBLIC_TINYBIRD_HOST&project-name=tinybird-signatures-dashboard&repository-name=tinybird-signatures-dashboard) 12 | 13 | You'll need to enter your [Tinybird Token](https://www.tinybird.co/docs/concepts/auth-tokens) and Tinybird Host (e.g. https://ui.tinybird.co). 14 | 15 | -------------------------------------------------------------------------------- /app/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinybirdco/demo-user-facing-saas-dashboard-signatures/ce751655649f7c175719201df2ed4ab64fecf517/app/app/favicon.ico -------------------------------------------------------------------------------- /app/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-rgb: 255, 255, 255; 8 | } 9 | 10 | body { 11 | color: rgb(var(--foreground-rgb)); 12 | background: rgba(--background-rgb); 13 | } 14 | 15 | @layer utilities { 16 | .text-balance { 17 | text-wrap: balance; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata = { 8 | title: "Tinybird Demo - User-facing dashboard", 9 | description: "A user-facing dashboard demo using Tinybird", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | 20 |
21 |

Tinybird Demo - User-facing dashboard

22 | {children} 23 |
24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /app/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { BarChart, Card, Subtitle, Text, Title } from "@tremor/react"; 4 | import React from "react"; 5 | import useSWR from "swr"; 6 | import { useFetcher } from "@/hooks/useFetch"; 7 | 8 | 9 | // Get your Tinybird host and token from the .env file 10 | const TINYBIRD_HOST = process.env.NEXT_PUBLIC_TINYBIRD_HOST; // The host URL for the Tinybird API 11 | 12 | const REFRESH_INTERVAL_IN_MILLISECONDS = 5000; // five seconds 13 | 14 | export default function Dashboard() { 15 | // Define date range for the query 16 | const today = new Date(); // Get today's date 17 | const dateFrom = new Date(today.setMonth(today.getMonth() - 1)); // Start the query's dateFrom to the one month before today 18 | const dateTo = new Date(today.setMonth(today.getMonth() + 1)); // Set the query's dateTo to be one month from today 19 | 20 | // Format for passing as a query parameter 21 | const dateFromFormatted = dateFrom.toISOString().substring(0, 10); 22 | const dateToFormatted = dateTo.toISOString().substring(0, 10); 23 | 24 | const fetcher = useFetcher(); // This fetcher handles the token revalidation 25 | 26 | // Constructing the URL for fetching data, including host, token, and date range 27 | const endpointUrl = new URL( 28 | "/v0/pipes/ranking_of_top_organizations_creating_signatures.json", 29 | TINYBIRD_HOST 30 | ); 31 | endpointUrl.searchParams.set("date_from", dateFromFormatted); 32 | endpointUrl.searchParams.set("date_to", dateToFormatted); 33 | 34 | // Initializes variables for storing data 35 | let ranking_of_top_organizations_creating_signatures, latency, errorMessage; 36 | 37 | // Using SWR hook to handle state and refresh result every five seconds 38 | const { data } = useSWR(endpointUrl.toString(), fetcher, { 39 | refreshInterval: REFRESH_INTERVAL_IN_MILLISECONDS, 40 | onError: (error) => (errorMessage = error), 41 | }); 42 | 43 | if (!data) return; 44 | 45 | if (data?.error) { 46 | errorMessage = data.error; 47 | return; 48 | } 49 | 50 | ranking_of_top_organizations_creating_signatures = data.data; // Setting the state with the fetched data 51 | latency = data.statistics?.elapsed; // Setting the state with the query latency from Tinybird 52 | 53 | return ( 54 | 55 | Top Organizations Creating Signatures 56 | Ranked from highest to lowest 57 | {ranking_of_top_organizations_creating_signatures && ( 58 | 67 | )} 68 | {latency && Latency: {latency * 1000} ms} 69 | {errorMessage && ( 70 |
71 |

72 | Oops, something happens: {errorMessage} 73 |

74 |

Check your console for more information

75 |
76 | )} 77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /app/hooks/useFetch.tsx: -------------------------------------------------------------------------------- 1 | import { generateJWT } from "@/server/token"; 2 | import { useState } from "react"; 3 | 4 | export function useFetcher() { 5 | const [token, setToken] = useState(""); 6 | 7 | // Generate a new JWT token and store the new token in the state 8 | const refreshToken = async () => { 9 | const newToken = await generateJWT(); 10 | setToken(newToken); 11 | return newToken; 12 | }; 13 | 14 | // Function to fetch data from Tinybird URL with a JWT token 15 | // If the token expires, a new one is generated in the server 16 | return async (url: string) => { 17 | let currentToken = token; 18 | if (!currentToken) { 19 | currentToken = await refreshToken(); 20 | } 21 | const response = await fetch(url + "&token=" + currentToken); 22 | 23 | if (response.status === 200) { 24 | return response.json(); 25 | } 26 | if (response.status === 403) { 27 | const newToken = await refreshToken(); 28 | return fetch(url + "&token=" + newToken).then((res) => res.json()); 29 | } 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /app/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinybirdco/demo-user-facing-analytics-signatures-dashboard", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "@faker-js/faker": "^8.4.1", 12 | "@headlessui/react": "^1.7.19", 13 | "@headlessui/tailwindcss": "^0.2.0", 14 | "@types/jsonwebtoken": "^9.0.6", 15 | "jsonwebtoken": "^9.0.2", 16 | "@tremor/react": "^3.16.2", 17 | "next": "14.2.3", 18 | "react": "^18", 19 | "react-dom": "^18", 20 | "swr": "^2.2.5" 21 | }, 22 | "devDependencies": { 23 | "@tailwindcss/forms": "^0.5.7", 24 | "@types/node": "^20", 25 | "@types/react": "^18", 26 | "@types/react-dom": "^18", 27 | "eslint": "^8", 28 | "eslint-config-next": "14.2.3", 29 | "postcss": "^8", 30 | "tailwindcss": "^3.4.1", 31 | "typescript": "^5" 32 | }, 33 | "license": "MIT", 34 | "author": "Tinybird team" 35 | } 36 | -------------------------------------------------------------------------------- /app/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /app/server/token.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import jwt from "jsonwebtoken"; 4 | 5 | const TINYBIRD_SIGNING_TOKEN = process.env.TINYBIRD_SIGNING_TOKEN ?? ""; 6 | const WORKSPACE_ID = process.env.TINYBIRD_WORKSPACE ?? ""; // Get this ID by running `tb workspace current` 7 | const PIPE_ID = "ranking_of_top_organizations_creating_signatures"; // The name of the pipe you want to consume 8 | 9 | // Server function that generates a JWT 10 | // All the Tinybird related data won't be visible in the browser 11 | export async function generateJWT() { 12 | const next10minutes = new Date(); 13 | next10minutes.setTime(next10minutes.getTime() + 1000 * 60 * 10); 14 | 15 | const payload = { 16 | workspace_id: WORKSPACE_ID, 17 | name: "my_demo_jwt", 18 | exp: next10minutes.getTime() / 1000, // Token only valid for the next 10 minutes 19 | scopes: [ 20 | { 21 | type: "PIPES:READ", 22 | resource: PIPE_ID, 23 | }, 24 | ], 25 | }; 26 | 27 | return jwt.sign(payload, TINYBIRD_SIGNING_TOKEN); 28 | } 29 | -------------------------------------------------------------------------------- /app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | /* eslint-disable max-len */ 3 | const colors = require('tailwindcss/colors'); 4 | module.exports = { 5 | darkMode: 'class', 6 | content: [ 7 | "./app/**/*.{js,ts,jsx,tsx}", 8 | "./pages/**/*.{js,ts,jsx,tsx}", 9 | "./components/**/*.{js,ts,jsx,tsx}", 10 | "./node_modules/@tremor/**/*.{js,ts,jsx,tsx}", 11 | ], 12 | theme: { 13 | transparent: "transparent", 14 | current: "currentColor", 15 | extend: { 16 | colors: { 17 | // light mode 18 | tremor: { 19 | brand: { 20 | faint: colors.blue[50], 21 | muted: colors.blue[200], 22 | subtle: colors.blue[400], 23 | DEFAULT: colors.blue[500], 24 | emphasis: colors.blue[700], 25 | inverted: colors.white, 26 | }, 27 | background: { 28 | muted: colors.gray[50], 29 | subtle: colors.gray[100], 30 | DEFAULT: colors.white, 31 | emphasis: colors.gray[700], 32 | }, 33 | border: { 34 | DEFAULT: colors.gray[200], 35 | }, 36 | ring: { 37 | DEFAULT: colors.gray[200], 38 | }, 39 | content: { 40 | subtle: colors.gray[400], 41 | DEFAULT: colors.gray[500], 42 | emphasis: colors.gray[700], 43 | strong: colors.gray[900], 44 | inverted: colors.white, 45 | }, 46 | }, 47 | }, 48 | boxShadow: { 49 | // light 50 | "tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)", 51 | "tremor-card": "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", 52 | "tremor-dropdown": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", 53 | }, 54 | borderRadius: { 55 | "tremor-small": "0.375rem", 56 | "tremor-default": "0.5rem", 57 | "tremor-full": "9999px", 58 | }, 59 | fontSize: { 60 | "tremor-label": ["0.75rem", { lineHeight: "1rem" }], 61 | "tremor-default": ["0.875rem", { lineHeight: "1.25rem" }], 62 | "tremor-title": ["1.125rem", { lineHeight: "1.75rem" }], 63 | "tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }], 64 | }, 65 | }, 66 | }, 67 | safelist: [ 68 | { 69 | pattern: 70 | /^(bg-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, 71 | variants: ["hover", "ui-selected"], 72 | }, 73 | { 74 | pattern: 75 | /^(text-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, 76 | variants: ["hover", "ui-selected"], 77 | }, 78 | { 79 | pattern: 80 | /^(border-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, 81 | variants: ["hover", "ui-selected"], 82 | }, 83 | { 84 | pattern: 85 | /^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, 86 | }, 87 | { 88 | pattern: 89 | /^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, 90 | }, 91 | { 92 | pattern: 93 | /^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, 94 | }, 95 | ], 96 | plugins: [require('@headlessui/tailwindcss'), require('@tailwindcss/forms')], 97 | }; 98 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /datagen/.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 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 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /datagen/README.md: -------------------------------------------------------------------------------- 1 | # Data Generator 2 | 3 | This folder contains code to generate data for this demo. 4 | 5 | ## How to generate data 6 | 7 | Run the following from this directory: 8 | 9 | ```sh 10 | npm install 11 | npm run seed 12 | ``` 13 | -------------------------------------------------------------------------------- /datagen/mockDataGenerator.js: -------------------------------------------------------------------------------- 1 | import { send_data_to_tinybird, read_tinyb_config } from "./utils/tinybird.js"; 2 | import { faker } from "@faker-js/faker"; 3 | 4 | let account_id_list = []; 5 | 6 | const generateSignaturePayload = ( 7 | account_id, 8 | status, 9 | signatureType, 10 | signature_id, 11 | since, 12 | until, 13 | created_on 14 | ) => { 15 | // Types of electron signatures 16 | // Simple - Sign with one click or enter a PIN sent via SMS. 17 | // Advance(biometrics) - Done with a pen stroke, just like signing on paper. 18 | // Advance(digital certificate) - The signatory uses their digital certificate, issued by third parties. 19 | // Qualified - The signatory uses a digital certificate issued by Signaturit. 20 | 21 | return { 22 | signature_id, 23 | account_id, 24 | status, 25 | signatureType, 26 | since: since.toISOString().substring(0, 10), 27 | until: until.toISOString().substring(0, 10), 28 | created_on: created_on.toISOString().substring(0, 10), 29 | timestamp: Date.now(), 30 | uuid: faker.string.uuid(), 31 | }; 32 | }; 33 | 34 | const generateAccountPayload = () => { 35 | const status = ["active", "inactive", "pending"]; 36 | const id = faker.number.int({ min: 10000, max: 99999 }); 37 | account_id_list.push(id); 38 | 39 | return { 40 | account_id: id, 41 | organization: faker.company.name(), 42 | status: status[faker.number.int({ min: 0, max: 2 })], 43 | role: faker.person.jobTitle(), 44 | certified_SMS: faker.datatype.boolean(), 45 | phone: faker.phone.number(), 46 | email: faker.internet.email(), 47 | person: faker.person.fullName(), 48 | certified_email: faker.datatype.boolean(), 49 | photo_id_certified: faker.datatype.boolean(), 50 | created_on: faker.date 51 | .between({ from: "2020-01-01", to: "2023-12-31" }) 52 | .toISOString() 53 | .substring(0, 10), 54 | timestamp: Date.now(), 55 | }; 56 | }; 57 | 58 | // Generates typcial dataflow for a signature, it looks like this 59 | // Someone creates the signature and requests for signs 60 | // One person sings 61 | // Other person signs 62 | // The signature is finished (complete, expired, canceled, declined, error) 63 | 64 | async function sendMessageAtRandomInterval(token, callback) { 65 | let randomInterval = faker.number.int({ min: 10, max: 50 }); 66 | 67 | setTimeout(() => { 68 | const signatureTypeList = [ 69 | "simple", 70 | "advance(biometrics)", 71 | "advance(digital certificate)", 72 | "qualified", 73 | ]; 74 | const signatureType = 75 | signatureTypeList[faker.number.int({ min: 0, max: 3 })]; 76 | let signatureID = faker.string.uuid(); 77 | 78 | let created_on = faker.date.past({ years: 3 }); 79 | let since = faker.date.soon({ days: 3, refDate: created_on }); 80 | let until = faker.date.soon({ days: 7, refDate: since }); 81 | 82 | callback(token, signatureID, signatureType, since, until, created_on) 83 | .then(() => sendMessageAtRandomInterval(token, callback)) 84 | .catch((err) => console.error(err)); // Catch any errors from async operations 85 | }, randomInterval); 86 | } 87 | 88 | const generateTinybirdPayload = async ( 89 | token, 90 | signatureID, 91 | signatureType, 92 | since, 93 | until, 94 | created_on 95 | ) => { 96 | const statusList = [ 97 | "in_queue", 98 | "ready", 99 | "signing", 100 | "completed", 101 | "expired", 102 | "canceled", 103 | "declined", 104 | "error", 105 | ]; 106 | 107 | const accountPayload = generateAccountPayload(); 108 | await send_data_to_tinybird("accounts", token, accountPayload); 109 | console.log("Sending account data to Tinybird"); 110 | 111 | const accountId1 = 112 | account_id_list[ 113 | faker.number.int({ min: 0, max: account_id_list.length - 1 }) 114 | ]; 115 | created_on = faker.date.soon({ days: 1, refDate: created_on }); 116 | // status either in_queue or ready 117 | let subscriptionPayload = generateSignaturePayload( 118 | accountId1, 119 | statusList[faker.number.int({ min: 0, max: 1 })], 120 | signatureType, 121 | signatureID, 122 | since, 123 | until, 124 | created_on 125 | ); 126 | await send_data_to_tinybird("signatures", token, subscriptionPayload); 127 | console.log("Sending signature data to Tinybird"); 128 | 129 | const accountId2 = 130 | account_id_list[ 131 | faker.number.int({ min: 0, max: account_id_list.length - 1 }) 132 | ]; 133 | created_on = faker.date.soon({ days: 1, refDate: created_on }); 134 | subscriptionPayload = generateSignaturePayload( 135 | accountId2, 136 | "signing", 137 | signatureType, 138 | signatureID, 139 | since, 140 | until, 141 | created_on 142 | ); 143 | await send_data_to_tinybird("signatures", token, subscriptionPayload); 144 | console.log("Sending signature data to Tinybird"); 145 | 146 | // const accountId3 = account_id_list[faker.number.int({ min: 0, max: account_id_list.length - 1 })]; 147 | // created_on = faker.date.soon({ days: 2, refDate: created_on }) 148 | // subscriptionPayload = generateSignaturePayload(accountId3, 'signing', signatureType, signatureID, since, until, created_on); 149 | // await send_data_to_tinybird("signatures", token, subscriptionPayload); 150 | // console.log('Sending signature data to Tinybird'); 151 | 152 | const finalStatus = faker.helpers.weightedArrayElement([ 153 | { weight: 7.5, value: "completed" }, 154 | { weight: 1, value: "expired" }, 155 | { weight: 0.5, value: "canceled" }, 156 | { weight: 0.5, value: "declined" }, 157 | { weight: 0.5, value: "error" }, 158 | ]); // 7.5/10 chance of being completed, 1/10 chance of being expired, 0.5/10 chance of being canceled, declined or error 159 | created_on = faker.date.soon({ days: 7, refDate: created_on }); 160 | subscriptionPayload = generateSignaturePayload( 161 | accountId1, 162 | finalStatus, 163 | signatureType, 164 | signatureID, 165 | since, 166 | until, 167 | created_on 168 | ); 169 | await send_data_to_tinybird("signatures", token, subscriptionPayload); 170 | console.log("Sending signature data to Tinybird"); 171 | }; 172 | 173 | const main = async () => { 174 | try { 175 | console.log("Sending data to Tinybird"); 176 | const token = await read_tinyb_config("../.tinyb"); 177 | 178 | console.log("Initial seeding of account data to Tinybird"); 179 | for (let i = 0; i < 10; i++) { 180 | const accountPayload = generateAccountPayload(); 181 | await send_data_to_tinybird("accounts", token, accountPayload); 182 | console.log("Sending account data to Tinybird"); 183 | } 184 | console.log("Initial seeding complete"); 185 | await sendMessageAtRandomInterval( 186 | token, 187 | async (token, signatureID, signatureType, since, until, created_on) => { 188 | generateTinybirdPayload( 189 | token, 190 | signatureID, 191 | signatureType, 192 | since, 193 | until, 194 | created_on 195 | ); 196 | } 197 | ); 198 | console.log("Data sent to Tinybird"); 199 | } catch (error) { 200 | console.error(error); 201 | } 202 | }; 203 | 204 | await main(); 205 | -------------------------------------------------------------------------------- /datagen/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinybirdco/demo-user-facing-analytics-signatures-datagen", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@tinybirdco/demo-user-facing-analytics-signatures-datagen", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@faker-js/faker": "^8.4.1" 13 | } 14 | }, 15 | "node_modules/@faker-js/faker": { 16 | "version": "8.4.1", 17 | "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", 18 | "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", 19 | "funding": [ 20 | { 21 | "type": "opencollective", 22 | "url": "https://opencollective.com/fakerjs" 23 | } 24 | ], 25 | "engines": { 26 | "node": "^14.17.0 || ^16.13.0 || >=18.0.0", 27 | "npm": ">=6.14.13" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /datagen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinybirdco/demo-user-facing-analytics-signatures-datagen", 3 | "version": "1.0.0", 4 | "description": "This folder contains code to generate data for this demo.", 5 | "main": "mockDataGenerator.js", 6 | "type": "module", 7 | "scripts": { 8 | "seed": "node ./mockDataGenerator.js" 9 | }, 10 | "dependencies": { 11 | "@faker-js/faker": "^8.4.1" 12 | }, 13 | "license": "MIT", 14 | "author": "Tinybird team" 15 | } 16 | -------------------------------------------------------------------------------- /datagen/utils/tinybird.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | // function that reads in .tinyb file and returns the token 4 | export async function read_tinyb_config(path) { 5 | return new Promise((resolve, reject) => { 6 | fs.readFile(path, "utf8", (err, jsonString) => { 7 | if (err) { 8 | console.log("File read failed. Try reauthenticating with the Tinybird CLI:", err); 9 | reject(err); 10 | } 11 | const token = JSON.parse(jsonString).token; 12 | resolve(token); 13 | }); 14 | }); 15 | } 16 | 17 | export async function send_data_to_tinybird(name, token, payload) { 18 | const events_url = "https://api.tinybird.co/v0/events?name="; 19 | 20 | return fetch(events_url + name, { 21 | method: "POST", 22 | body: JSON.stringify(payload), 23 | headers: { 24 | Authorization: `Bearer ${token}`, 25 | }, 26 | }) 27 | .then((res) => res.json()) 28 | .catch((error) => console.log(error)); 29 | } -------------------------------------------------------------------------------- /img/app_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinybirdco/demo-user-facing-saas-dashboard-signatures/ce751655649f7c175719201df2ed4ab64fecf517/img/app_screenshot.png -------------------------------------------------------------------------------- /img/data_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinybirdco/demo-user-facing-saas-dashboard-signatures/ce751655649f7c175719201df2ed4ab64fecf517/img/data_flow.png -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-user-facing-saas-dashboard-signatures", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /tinybird/README.md: -------------------------------------------------------------------------------- 1 | # Tinybird Data Project 2 | 3 | This folder contains all of the resources to build the Tinybird data project for this demo. 4 | 5 | ## Data Project Structure 6 | 7 | This data project contains X Data Sources, Y Endpoints, and X additional Pipes. 8 | 9 | ``` 10 | ├── datasources 11 | │ └── accounts.datasource 12 | │ └── signatures.datasource 13 | ├── endpoints 14 | │ └── ranking_of_top_organizations_creating_signatures.pipe 15 | ``` 16 | 17 | ### Lineage Graph 18 | 19 | ![Tinybird Data Flow](/img/data_flow.png "Tinybird Data Flow") 20 | 21 | ## Deploying the Tinybird resources 22 | 23 | To deploy these resources to the server, run the following from this directory: 24 | 25 | ```sh 26 | tb auth --token 27 | tb push --force 28 | ``` 29 | -------------------------------------------------------------------------------- /tinybird/datasources/accounts.datasource: -------------------------------------------------------------------------------- 1 | 2 | SCHEMA > 3 | `account_id` Int32 `json:$.account_id`, 4 | `certified_SMS` UInt8 `json:$.certified_SMS`, 5 | `certified_email` UInt8 `json:$.certified_email`, 6 | `created_on` Date `json:$.created_on`, 7 | `email` String `json:$.email`, 8 | `organization` String `json:$.organization`, 9 | `person` String `json:$.person`, 10 | `phone` String `json:$.phone`, 11 | `photo_id_certified` UInt8 `json:$.photo_id_certified`, 12 | `role` String `json:$.role`, 13 | `status` String `json:$.status`, 14 | `timestamp` Int64 `json:$.timestamp` 15 | 16 | ENGINE "MergeTree" 17 | ENGINE_PARTITION_KEY "toYear(created_on)" 18 | ENGINE_SORTING_KEY "created_on, role, status, timestamp" 19 | -------------------------------------------------------------------------------- /tinybird/datasources/signatures.datasource: -------------------------------------------------------------------------------- 1 | 2 | SCHEMA > 3 | `account_id` Int32 `json:$.account_id`, 4 | `created_on` Date `json:$.created_on`, 5 | `signatureType` String `json:$.signatureType`, 6 | `signature_id` String `json:$.signature_id`, 7 | `since` Date `json:$.since`, 8 | `status` String `json:$.status`, 9 | `timestamp` Int64 `json:$.timestamp`, 10 | `until` Date `json:$.until`, 11 | `uuid` String `json:$.uuid` 12 | 13 | ENGINE "MergeTree" 14 | ENGINE_PARTITION_KEY "toYear(created_on)" 15 | ENGINE_SORTING_KEY "created_on, status, timestamp, uuid" 16 | -------------------------------------------------------------------------------- /tinybird/pipes/ranking_of_top_organizations_creating_signatures.pipe: -------------------------------------------------------------------------------- 1 | TOKEN "ranking_of_top_organizations_creating_signatures_endpoint_read_7713" READ 2 | 3 | NODE retrieve_signatures 4 | SQL > 5 | 6 | % 7 | SELECT 8 | account_id, 9 | {% if defined(completed) %} countIf(status = 'completed') total 10 | {% else %} count() total 11 | {% end %} 12 | FROM signatures 13 | WHERE 14 | fromUnixTimestamp64Milli(timestamp) 15 | BETWEEN {{ 16 | Date( 17 | date_from, 18 | '2023-01-01', 19 | description="Initial date", 20 | required=True, 21 | ) 22 | }} 23 | AND {{ Date(date_to, '2024-01-01', description="End date", required=True) }} 24 | GROUP BY account_id 25 | HAVING total > 0 26 | ORDER BY total DESC 27 | 28 | NODE endpoint 29 | SQL > 30 | 31 | % 32 | SELECT organization, sum(total) AS org_total 33 | FROM retrieve_signatures 34 | LEFT JOIN accounts ON accounts.account_id = retrieve_signatures.account_id 35 | GROUP BY organization 36 | ORDER BY org_total DESC 37 | LIMIT {{ Int8(limit, 10, description="The number of rows accounts to retrieve", required=False) }} 38 | --------------------------------------------------------------------------------