├── .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 | 
6 |
7 | ## Deploying the app
8 |
9 | This is a NextJS application. Deploy it to the cloud with Vercel.
10 |
11 | [](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 | 
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 |
--------------------------------------------------------------------------------