├── .nvmrc
├── packages
├── pinorama-client
│ ├── README.md
│ ├── tests
│ │ └── .gitkeep
│ ├── src
│ │ └── platform
│ │ │ ├── node.ts
│ │ │ └── browser.ts
│ ├── tsconfig.json
│ ├── CHANGELOG.md
│ ├── package.json
│ └── rollup.config.mjs
├── pinorama-server
│ ├── README.md
│ ├── src
│ │ ├── plugins
│ │ │ ├── index.mts
│ │ │ ├── auth.mts
│ │ │ └── graceful-save.mts
│ │ ├── routes
│ │ │ ├── index.mts
│ │ │ ├── introspection.mts
│ │ │ ├── persist.mts
│ │ │ ├── bulk.mts
│ │ │ ├── search.mts
│ │ │ └── styles.mts
│ │ ├── utils
│ │ │ ├── metadata.mts
│ │ │ └── styles.mts
│ │ └── index.mts
│ ├── tsconfig.json
│ ├── CHANGELOG.md
│ └── package.json
├── pinorama-studio
│ ├── src
│ │ ├── components
│ │ │ ├── kbd
│ │ │ │ ├── index.ts
│ │ │ │ └── kbd.tsx
│ │ │ ├── title-bar
│ │ │ │ ├── index.ts
│ │ │ │ ├── components
│ │ │ │ │ ├── connection-toggle-button.tsx
│ │ │ │ │ ├── pinorama-logo.tsx
│ │ │ │ │ ├── settings-button.tsx
│ │ │ │ │ ├── theme-toggle-button.tsx
│ │ │ │ │ ├── hotkeys-button.tsx
│ │ │ │ │ └── connection-status-button.tsx
│ │ │ │ └── title-bar.tsx
│ │ │ ├── empty-state
│ │ │ │ ├── index.ts
│ │ │ │ └── empty-state.tsx
│ │ │ ├── error-state
│ │ │ │ ├── index.ts
│ │ │ │ └── error-state.tsx
│ │ │ ├── icon-button
│ │ │ │ ├── index.ts
│ │ │ │ └── icon-button.tsx
│ │ │ ├── loading-state
│ │ │ │ ├── index.ts
│ │ │ │ └── loading-state.tsx
│ │ │ ├── search-input
│ │ │ │ ├── index.ts
│ │ │ │ └── search-input.tsx
│ │ │ ├── clipboard-button
│ │ │ │ ├── index.ts
│ │ │ │ └── clipboard-button.tsx
│ │ │ └── ui
│ │ │ │ ├── skeleton.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── popover.tsx
│ │ │ │ ├── tooltip.tsx
│ │ │ │ ├── checkbox.tsx
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── toggle.tsx
│ │ │ │ ├── resizable.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── table.tsx
│ │ │ │ ├── dialog.tsx
│ │ │ │ └── form.tsx
│ │ ├── vite-env.d.ts
│ │ ├── hooks
│ │ │ ├── index.ts
│ │ │ ├── use-pinorama-introspection.ts
│ │ │ ├── use-pinorama-styles.ts
│ │ │ ├── use-module-hotkeys.ts
│ │ │ └── use-pinorama-connection.ts
│ │ ├── contexts
│ │ │ ├── index.ts
│ │ │ ├── pinorama-client-context.tsx
│ │ │ ├── i18n-context.tsx
│ │ │ ├── app-config-context.tsx
│ │ │ └── theme-context.tsx
│ │ ├── lib
│ │ │ ├── utils.tsx
│ │ │ ├── introspection.ts
│ │ │ └── modules.tsx
│ │ ├── main.tsx
│ │ ├── app.tsx
│ │ ├── modules
│ │ │ ├── log-explorer
│ │ │ │ ├── components
│ │ │ │ │ ├── log-filters
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ ├── lib
│ │ │ │ │ │ │ ├── utils.tsx
│ │ │ │ │ │ │ └── operations.ts
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── hooks
│ │ │ │ │ │ │ └── use-facet.ts
│ │ │ │ │ │ └── components
│ │ │ │ │ │ │ ├── facet-factory-input.tsx
│ │ │ │ │ │ │ ├── facet-item.tsx
│ │ │ │ │ │ │ ├── facet-header.tsx
│ │ │ │ │ │ │ ├── facet-body.tsx
│ │ │ │ │ │ │ └── facet.tsx
│ │ │ │ │ ├── log-viewer
│ │ │ │ │ │ ├── hooks
│ │ │ │ │ │ │ ├── use-static-logs.ts
│ │ │ │ │ │ │ └── use-live-logs.ts
│ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ ├── thead.tsx
│ │ │ │ │ │ │ ├── tbody.tsx
│ │ │ │ │ │ │ └── header
│ │ │ │ │ │ │ │ ├── toggle-live-button.tsx
│ │ │ │ │ │ │ │ ├── toggle-columns-button.tsx
│ │ │ │ │ │ │ │ └── header.tsx
│ │ │ │ │ │ └── utils
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── log-details
│ │ │ │ │ │ └── styles
│ │ │ │ │ │ └── json-viewer.css
│ │ │ │ ├── index.ts
│ │ │ │ ├── messages
│ │ │ │ │ ├── en.json
│ │ │ │ │ └── it.json
│ │ │ │ └── utils
│ │ │ │ │ └── index.ts
│ │ │ └── index.tsx
│ │ ├── i18n
│ │ │ ├── messages
│ │ │ │ ├── en.json
│ │ │ │ └── it.json
│ │ │ └── index.ts
│ │ ├── root.tsx
│ │ └── styles
│ │ │ └── globals.css
│ ├── postcss.config.js
│ ├── public
│ │ └── pinorama-logo.webp
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ ├── .gitignore
│ ├── components.json
│ ├── tsconfig.json
│ ├── index.html
│ ├── CHANGELOG.md
│ ├── tailwind.config.cjs
│ └── package.json
├── pinorama-docs
│ ├── .gitignore
│ ├── public
│ │ ├── pinorama-logo.webp
│ │ └── pinorama-screenshot.webp
│ ├── .vitepress
│ │ ├── theme
│ │ │ └── index.ts
│ │ ├── config
│ │ │ ├── index.ts
│ │ │ └── en.ts
│ │ └── style
│ │ │ └── vars.css
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── index.md
│ └── guide
│ │ ├── quick-start.md
│ │ └── index.md
├── pinorama-presets
│ ├── src
│ │ ├── presets
│ │ │ ├── index.mts
│ │ │ ├── pino.mts
│ │ │ └── fastify.mts
│ │ └── utils.mts
│ ├── CHANGELOG.md
│ ├── tsconfig.json
│ └── package.json
├── pinorama-transport
│ ├── tests
│ │ ├── cli.test.mts
│ │ ├── unit.test.mts
│ │ └── integration.test.mts
│ ├── CHANGELOG.md
│ ├── tsconfig.json
│ ├── package.json
│ └── src
│ │ ├── lib.mts
│ │ └── cli.mts
└── pinorama-types
│ ├── CHANGELOG.md
│ ├── tsconfig.json
│ ├── package.json
│ └── src
│ └── index.ts
├── .github
├── FUNDING.yml
└── workflows
│ ├── turbo.yml
│ ├── docs.yml
│ └── release.yml
├── .husky
├── pre-commit
└── commit-msg
├── .npmrc
├── pnpm-workspace.yaml
├── commitlint.config.cjs
├── .changeset
├── curvy-coins-kneel.md
├── clean-bobcats-care.md
├── hip-shrimps-rule.md
├── config.json
└── README.md
├── .editorconfig
├── turbo.json
├── examples
├── create-server-example
│ ├── package.json
│ ├── CHANGELOG.md
│ └── index.mjs
└── fastify-example
│ ├── package.json
│ ├── CHANGELOG.md
│ └── index.mjs
├── .gitignore
├── biome.json
├── .all-contributorsrc
├── package.json
└── README.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20
2 |
--------------------------------------------------------------------------------
/packages/pinorama-client/README.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/pinorama-server/README.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: cesconix
2 |
--------------------------------------------------------------------------------
/packages/pinorama-client/tests/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | pnpm biome
2 | pnpm test
3 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | pnpm commitlint --edit "$1"
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | enable-pre-post-scripts=true
2 | package-manager-strict=false
3 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/kbd/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./kbd"
2 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "packages/*"
3 | - "examples/*"
4 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/packages/pinorama-docs/.gitignore:
--------------------------------------------------------------------------------
1 | .vuepress/dist
2 | .vitepress/dist
3 | .vitepress/cache
4 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/title-bar/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./title-bar"
2 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/empty-state/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./empty-state"
2 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/error-state/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./error-state"
2 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/icon-button/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./icon-button"
2 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/loading-state/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./loading-state"
2 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/search-input/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./search-input"
2 |
--------------------------------------------------------------------------------
/commitlint.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["@commitlint/config-conventional"]
3 | }
4 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/clipboard-button/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./clipboard-button"
2 |
--------------------------------------------------------------------------------
/packages/pinorama-presets/src/presets/index.mts:
--------------------------------------------------------------------------------
1 | export * from "./fastify.mjs"
2 | export * from "./pino.mjs"
3 |
--------------------------------------------------------------------------------
/packages/pinorama-server/src/plugins/index.mts:
--------------------------------------------------------------------------------
1 | export * from "./auth.mjs"
2 | export * from "./graceful-save.mjs"
3 |
--------------------------------------------------------------------------------
/.changeset/curvy-coins-kneel.md:
--------------------------------------------------------------------------------
1 | ---
2 | "pinorama-studio": patch
3 | ---
4 |
5 | perf(studio): optimize table scrolling
6 |
--------------------------------------------------------------------------------
/packages/pinorama-transport/tests/cli.test.mts:
--------------------------------------------------------------------------------
1 | import { describe } from "vitest"
2 |
3 | describe.skip("cli", () => {})
4 |
--------------------------------------------------------------------------------
/packages/pinorama-client/src/platform/node.ts:
--------------------------------------------------------------------------------
1 | import { setTimeout } from "node:timers/promises"
2 |
3 | export { setTimeout }
4 |
--------------------------------------------------------------------------------
/.changeset/clean-bobcats-care.md:
--------------------------------------------------------------------------------
1 | ---
2 | "pinorama-server": minor
3 | "pinorama-studio": minor
4 | ---
5 |
6 | update to orama v3
7 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {}
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/pinorama-docs/public/pinorama-logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pinoramajs/pinorama/HEAD/packages/pinorama-docs/public/pinorama-logo.webp
--------------------------------------------------------------------------------
/packages/pinorama-studio/public/pinorama-logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pinoramajs/pinorama/HEAD/packages/pinorama-studio/public/pinorama-logo.webp
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 |
--------------------------------------------------------------------------------
/packages/pinorama-docs/public/pinorama-screenshot.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pinoramajs/pinorama/HEAD/packages/pinorama-docs/public/pinorama-screenshot.webp
--------------------------------------------------------------------------------
/packages/pinorama-client/src/platform/browser.ts:
--------------------------------------------------------------------------------
1 | const setTimeout = (ms: number) =>
2 | new Promise((resolve) => window.setTimeout(resolve, ms))
3 |
4 | export { setTimeout }
5 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./use-pinorama-connection"
2 | export * from "./use-pinorama-introspection"
3 | export * from "./use-pinorama-styles"
4 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/contexts/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./app-config-context"
2 | export * from "./i18n-context"
3 | export * from "./pinorama-client-context"
4 | export * from "./theme-context"
5 |
--------------------------------------------------------------------------------
/.changeset/hip-shrimps-rule.md:
--------------------------------------------------------------------------------
1 | ---
2 | "pinorama-create-server-example": minor
3 | "pinorama-fastify-example": minor
4 | "pinorama-server": minor
5 | "pinorama-studio": minor
6 | ---
7 |
8 | update to fastify v5
9 |
--------------------------------------------------------------------------------
/packages/pinorama-server/src/routes/index.mts:
--------------------------------------------------------------------------------
1 | export * from "./bulk.mjs"
2 | export * from "./introspection.mjs"
3 | export * from "./persist.mjs"
4 | export * from "./search.mjs"
5 | export * from "./styles.mjs"
6 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/lib/utils.tsx:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/packages/pinorama-types/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # pinorama-types
2 |
3 | ## 0.1.0
4 |
5 | ### Minor Changes
6 |
7 | - 4980b4b: add preset feature
8 |
9 | ### Patch Changes
10 |
11 | - a0495f4: add column visibility and sizing
12 |
--------------------------------------------------------------------------------
/packages/pinorama-presets/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # pinorama-presets
2 |
3 | ## 0.1.0
4 |
5 | ### Minor Changes
6 |
7 | - 4980b4b: add preset feature
8 |
9 | ### Patch Changes
10 |
11 | - a0495f4: add column visibility and sizing
12 |
--------------------------------------------------------------------------------
/packages/pinorama-docs/.vitepress/theme/index.ts:
--------------------------------------------------------------------------------
1 | import type { Theme } from "vitepress"
2 | import DefaultTheme from "vitepress/theme"
3 | import "../style/vars.css"
4 |
5 | export default {
6 | extends: DefaultTheme
7 | } satisfies Theme
8 |
--------------------------------------------------------------------------------
/packages/pinorama-docs/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # pinorama-docs
2 |
3 | ## 0.2.0
4 |
5 | ### Minor Changes
6 |
7 | - f28dea5: Home page redesign
8 |
9 | ## 0.1.1
10 |
11 | ### Patch Changes
12 |
13 | - fafcb13: chore(docs): add pinorama github link
14 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom/client"
2 | import { RootComponent } from "./root"
3 |
4 | const appElement = document.getElementById("app") as HTMLElement
5 | ReactDOM.createRoot(appElement).render()
6 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
3 | "changelog": "@changesets/cli/changelog",
4 | "commit": false,
5 | "fixed": [],
6 | "linked": [],
7 | "access": "restricted",
8 | "baseBranch": "main",
9 | "updateInternalDependencies": "patch",
10 | "ignore": []
11 | }
12 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/app.tsx:
--------------------------------------------------------------------------------
1 | import { TitleBar } from "@/components/title-bar"
2 | import { Outlet } from "@tanstack/react-router"
3 |
4 | export default function App() {
5 | return (
6 |
7 |
8 |
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/packages/pinorama-types/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ESNext",
5 | "moduleResolution": "node",
6 | "declaration": true,
7 | "outDir": "./dist",
8 | "strict": true,
9 | "esModuleInterop": true
10 | },
11 | "include": ["src"],
12 | "exclude": ["node_modules", "dist"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-filters/types.ts:
--------------------------------------------------------------------------------
1 | export type FacetValue = {
2 | value: string | number
3 | count: number
4 | }
5 |
6 | export type StringFilter = string[]
7 | export type EnumFilter = { in: (string | number)[] }
8 | export type FacetFilter = StringFilter | EnumFilter
9 |
10 | export type SearchFilters = Record
11 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "node:path"
2 | import react from "@vitejs/plugin-react"
3 | import { defineConfig } from "vite"
4 | import version from "vite-plugin-package-version"
5 |
6 | export default defineConfig({
7 | plugins: [react(), version()],
8 | resolve: {
9 | alias: {
10 | "@": path.resolve(__dirname, "./src")
11 | }
12 | }
13 | })
14 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/packages/pinorama-server/src/routes/introspection.mts:
--------------------------------------------------------------------------------
1 | import type { FastifyInstance } from "fastify"
2 |
3 | export async function introspectionRoute(fastify: FastifyInstance) {
4 | const { introspection } = fastify.pinoramaOpts
5 |
6 | fastify.route({
7 | url: "/introspection",
8 | method: "get",
9 | handler: async () => {
10 | return introspection
11 | }
12 | })
13 | }
14 |
--------------------------------------------------------------------------------
/packages/pinorama-transport/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # pinorama-transport
2 |
3 | ## 0.1.3
4 |
5 | ### Patch Changes
6 |
7 | - 4980b4b: add preset feature
8 |
9 | ## 0.1.2
10 |
11 | ### Patch Changes
12 |
13 | - b31f88c: execute build step on github release workflow
14 |
15 | ## 0.1.1
16 |
17 | ### Patch Changes
18 |
19 | - ed89795: Add repository, bugs, homepage, and author info in `package.json` file
20 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/app.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/loading-state/loading-state.tsx:
--------------------------------------------------------------------------------
1 | import { LoaderIcon } from "lucide-react"
2 | import { FormattedMessage } from "react-intl"
3 |
4 | export function LoadingState() {
5 | return (
6 |
7 |
8 |
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/packages/pinorama-presets/src/utils.mts:
--------------------------------------------------------------------------------
1 | import type { AnySchema } from "@orama/orama"
2 | import type { PinoramaIntrospection } from "pinorama-types"
3 |
4 | type PinoramaPreset = {
5 | schema: T
6 | introspection: PinoramaIntrospection
7 | }
8 |
9 | export function createPreset(
10 | schema: T,
11 | introspection: PinoramaIntrospection
12 | ): PinoramaPreset {
13 | return { schema, introspection }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/pinorama-server/src/plugins/auth.mts:
--------------------------------------------------------------------------------
1 | import type { FastifyInstance } from "fastify"
2 |
3 | export async function authHook(fastify: FastifyInstance) {
4 | const { adminSecret } = fastify.pinoramaOpts
5 |
6 | fastify.addHook("preHandler", async (req, res) => {
7 | if (adminSecret && req.headers["x-pinorama-admin-secret"] !== adminSecret) {
8 | res.code(401).send({ error: "unauthorized" })
9 | throw new Error("unauthorized")
10 | }
11 | })
12 | }
13 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "tasks": {
4 | "test": {
5 | "cache": false
6 | },
7 | "clean": {
8 | "dependsOn": ["^clean"],
9 | "cache": false
10 | },
11 | "build": {
12 | "dependsOn": ["^build"],
13 | "cache": false
14 | },
15 | "lint": {
16 | "dependsOn": ["^lint"]
17 | },
18 | "dev": {
19 | "cache": false,
20 | "persistent": true
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/kbd/kbd.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | export function Kbd({
4 | children,
5 | className
6 | }: { children: React.ReactNode; className?: string }) {
7 | return (
8 |
14 | {children}
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/index.tsx:
--------------------------------------------------------------------------------
1 | import type { Module } from "@/lib/modules"
2 | import LogExplorerModule from "./log-explorer"
3 |
4 | const modules: Module[] = [
5 | LogExplorerModule,
6 | {
7 | id: "feature-fake",
8 | routePath: "/feature2",
9 | component: () => (
10 |
11 | Welcome to Feature 2! 🎉
12 |
13 | )
14 | }
15 | ]
16 |
17 | export default modules
18 |
--------------------------------------------------------------------------------
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/packages/pinorama-server/src/utils/metadata.mts:
--------------------------------------------------------------------------------
1 | import type { AnyOrama } from "@orama/orama"
2 |
3 | export const withPinoramaMetadataSchema = (
4 | schema: AnyOrama["schema"]
5 | ): AnyOrama["schema"] => {
6 | return {
7 | ...schema,
8 | _pinorama: {
9 | createdAt: "number"
10 | }
11 | }
12 | }
13 |
14 | export const withPinoramaMetadataValue = (
15 | value: Record
16 | ): Record => {
17 | return {
18 | ...value,
19 | _pinorama: {
20 | createdAt: Date.now()
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/examples/create-server-example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pinorama-create-server-example",
3 | "version": "0.1.4",
4 | "type": "module",
5 | "private": true,
6 | "scripts": {
7 | "start": "node --inspect index.mjs",
8 | "clean": "rimraf node_modules"
9 | },
10 | "dependencies": {
11 | "@fastify/cors": "^10.0.2",
12 | "@fastify/one-line-logger": "^2.0.2",
13 | "fastify": "^5.2.1",
14 | "pinorama-server": "workspace:*",
15 | "pinorama-transport": "workspace:*"
16 | },
17 | "devDependencies": {
18 | "rimraf": "^5.0.7"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/examples/fastify-example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pinorama-fastify-example",
3 | "version": "0.1.4",
4 | "type": "module",
5 | "private": true,
6 | "scripts": {
7 | "start": "node --inspect index.mjs",
8 | "clean": "rimraf node_modules"
9 | },
10 | "dependencies": {
11 | "@fastify/cors": "^10.0.2",
12 | "@fastify/one-line-logger": "^2.0.2",
13 | "fastify": "^5.2.1",
14 | "pinorama-presets": "workspace:*",
15 | "pinorama-server": "workspace:*",
16 | "pinorama-transport": "workspace:*"
17 | },
18 | "devDependencies": {
19 | "rimraf": "^5.0.7"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/title-bar/components/connection-toggle-button.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import { usePinoramaConnection } from "@/hooks"
3 | import { FormattedMessage } from "react-intl"
4 |
5 | export function ConnectionToggleButton() {
6 | const { isConnected, toggleConnection } = usePinoramaConnection()
7 |
8 | return (
9 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/hooks/use-pinorama-introspection.ts:
--------------------------------------------------------------------------------
1 | import { usePinoramaClient } from "@/contexts"
2 | import type { AnySchema } from "@orama/orama"
3 | import { useQuery } from "@tanstack/react-query"
4 | import type { PinoramaIntrospection } from "pinorama-types"
5 |
6 | export const usePinoramaIntrospection = () => {
7 | const client = usePinoramaClient()
8 |
9 | return useQuery({
10 | queryKey: ["introspection", client],
11 | queryFn: () =>
12 | client?.introspection() as Promise>,
13 | staleTime: Number.POSITIVE_INFINITY
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/packages/pinorama-client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "esModuleInterop": true,
5 | "skipLibCheck": true,
6 | "target": "ES5",
7 | "allowJs": true,
8 | "resolveJsonModule": true,
9 | "moduleDetection": "force",
10 | "isolatedModules": true,
11 | "strict": true,
12 | "noUncheckedIndexedAccess": true,
13 | "moduleResolution": "Node",
14 | "module": "ESNext",
15 | "sourceMap": true,
16 | "lib": ["ESNext", "DOM"]
17 | },
18 | "include": ["src"],
19 | "exclude": ["node_modules", "dist", "tests"]
20 | }
21 |
--------------------------------------------------------------------------------
/.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 | # Local env files
9 | .env
10 | .env.local
11 | .env.development.local
12 | .env.test.local
13 | .env.production.local
14 |
15 | # Testing
16 | coverage
17 |
18 | # Turbo
19 | .turbo
20 |
21 | # Vercel
22 | .vercel
23 |
24 | # Build Outputs
25 | .next/
26 | out/
27 | build
28 | dist
29 |
30 |
31 | # Debug
32 | npm-debug.log*
33 | yarn-debug.log*
34 | yarn-error.log*
35 |
36 | # Misc
37 | .DS_Store
38 | *.pem
39 | cache
40 | logs
41 |
42 | # Orama
43 | orama_bump_*.json
44 |
--------------------------------------------------------------------------------
/packages/pinorama-server/src/plugins/graceful-save.mts:
--------------------------------------------------------------------------------
1 | import { persistToFile } from "@orama/plugin-data-persistence/server"
2 | import type { FastifyInstance } from "fastify"
3 |
4 | export async function gracefulSaveHook(fastify: FastifyInstance) {
5 | fastify.addHook("onClose", async (req) => {
6 | try {
7 | const savedPath = await persistToFile(
8 | fastify.pinoramaDb,
9 | fastify.pinoramaOpts.dbFormat,
10 | fastify.pinoramaOpts.dbPath
11 | )
12 | req.log.info(`database saved to ${savedPath}`)
13 | } catch (error) {
14 | req.log.error(`failed to save database: ${error}`)
15 | }
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/packages/pinorama-client/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # pinorama-client
2 |
3 | ## 0.2.1
4 |
5 | ### Patch Changes
6 |
7 | - 4980b4b: add preset feature
8 |
9 | ## 0.2.0
10 |
11 | ### Minor Changes
12 |
13 | - 306a952: add live mode feature
14 |
15 | ### Patch Changes
16 |
17 | - 0b8ae5b: chore: optimize code and improve type safety
18 |
19 | ## 0.1.3
20 |
21 | ### Patch Changes
22 |
23 | - b31f88c: execute build step on github release workflow
24 |
25 | ## 0.1.2
26 |
27 | ### Patch Changes
28 |
29 | - ed89795: Add repository, bugs, homepage, and author info in `package.json` file
30 |
31 | ## 0.1.1
32 |
33 | ### Patch Changes
34 |
35 | - 1059f37: replace "docs" with "logs"
36 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-filters/lib/utils.tsx:
--------------------------------------------------------------------------------
1 | import type { AnySchema } from "@orama/orama"
2 | import type { IntrospectionFacet, PinoramaIntrospection } from "pinorama-types"
3 |
4 | export const getFacetsConfig = (
5 | introspection: PinoramaIntrospection
6 | ) => {
7 | const definition: { name: string; type: IntrospectionFacet }[] = []
8 |
9 | const facets = introspection.facets
10 | if (!facets) return { definition }
11 |
12 | for (const [name, config] of Object.entries(facets)) {
13 | definition.push({
14 | name,
15 | type: config as IntrospectionFacet
16 | })
17 | }
18 |
19 | return { definition }
20 | }
21 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
3 | "organizeImports": {
4 | "enabled": true
5 | },
6 | "linter": {
7 | "enabled": true,
8 | "rules": {
9 | "recommended": true,
10 | "suspicious": {
11 | "noExplicitAny": "off"
12 | }
13 | }
14 | },
15 | "formatter": {
16 | "enabled": true,
17 | "indentStyle": "space",
18 | "lineWidth": 80
19 | },
20 | "javascript": {
21 | "formatter": {
22 | "trailingCommas": "none",
23 | "semicolons": "asNeeded"
24 | }
25 | },
26 | "vcs": {
27 | "enabled": true,
28 | "clientKind": "git",
29 | "useIgnoreFile": true
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/pinorama-docs/.vitepress/config/index.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitepress"
2 | import { en } from "./en"
3 |
4 | export default defineConfig({
5 | title: "Pinorama",
6 |
7 | lastUpdated: true,
8 | cleanUrls: true,
9 | metaChunk: true,
10 |
11 | head: [["link", { rel: "icon", href: "/pinorama-logo.webp" }]],
12 |
13 | themeConfig: {
14 | logo: "/pinorama-logo.webp",
15 |
16 | socialLinks: [
17 | { icon: "github", link: "https://github.com/pinoramajs/pinorama" }
18 | ],
19 |
20 | search: {
21 | provider: "local"
22 | }
23 | },
24 |
25 | locales: {
26 | root: { label: "English", ...en }
27 | },
28 |
29 | ignoreDeadLinks: true
30 | })
31 |
--------------------------------------------------------------------------------
/examples/fastify-example/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # pinorama-fastify-example
2 |
3 | ## 0.1.4
4 |
5 | ### Patch Changes
6 |
7 | - Updated dependencies [4980b4b]
8 | - pinorama-transport@0.1.3
9 | - pinorama-server@0.2.1
10 |
11 | ## 0.1.3
12 |
13 | ### Patch Changes
14 |
15 | - Updated dependencies [306a952]
16 | - Updated dependencies [f79e807]
17 | - pinorama-server@0.2.0
18 | - pinorama-transport@0.1.2
19 |
20 | ## 0.1.2
21 |
22 | ### Patch Changes
23 |
24 | - Updated dependencies [b31f88c]
25 | - pinorama-server@0.1.2
26 | - pinorama-transport@0.1.2
27 |
28 | ## 0.1.1
29 |
30 | ### Patch Changes
31 |
32 | - Updated dependencies [ed89795]
33 | - pinorama-transport@0.1.1
34 | - pinorama-server@0.1.1
35 |
--------------------------------------------------------------------------------
/packages/pinorama-presets/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "esModuleInterop": true,
5 | "skipLibCheck": true,
6 | "target": "es2022",
7 | "allowJs": true,
8 | "resolveJsonModule": true,
9 | "moduleDetection": "force",
10 | "isolatedModules": true,
11 | "strict": true,
12 | "noUncheckedIndexedAccess": true,
13 | "moduleResolution": "NodeNext",
14 | "module": "NodeNext",
15 | "sourceMap": true,
16 | "declaration": true,
17 | "declarationMap": true,
18 | "lib": ["ES2022"],
19 | "outDir": "dist"
20 | },
21 | "include": ["src"],
22 | "exclude": ["node_modules", "dist", "tests"]
23 | }
24 |
--------------------------------------------------------------------------------
/packages/pinorama-server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "esModuleInterop": true,
5 | "skipLibCheck": true,
6 | "target": "es2022",
7 | "allowJs": true,
8 | "resolveJsonModule": true,
9 | "moduleDetection": "force",
10 | "isolatedModules": true,
11 | "strict": true,
12 | "noUncheckedIndexedAccess": true,
13 | "moduleResolution": "NodeNext",
14 | "module": "NodeNext",
15 | "sourceMap": true,
16 | "declaration": true,
17 | "declarationMap": true,
18 | "lib": ["ES2022"],
19 | "outDir": "dist"
20 | },
21 | "include": ["src"],
22 | "exclude": ["node_modules", "dist", "tests"]
23 | }
24 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/title-bar/components/pinorama-logo.tsx:
--------------------------------------------------------------------------------
1 | import { FormattedMessage } from "react-intl"
2 |
3 | export function PinoramaLogo() {
4 | return (
5 |
6 |

7 |
8 | {" "}
9 |
10 |
14 |
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/examples/create-server-example/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # pinorama-create-server-example
2 |
3 | ## 0.1.4
4 |
5 | ### Patch Changes
6 |
7 | - Updated dependencies [4980b4b]
8 | - pinorama-transport@0.1.3
9 | - pinorama-server@0.2.1
10 |
11 | ## 0.1.3
12 |
13 | ### Patch Changes
14 |
15 | - Updated dependencies [306a952]
16 | - Updated dependencies [f79e807]
17 | - pinorama-server@0.2.0
18 | - pinorama-transport@0.1.2
19 |
20 | ## 0.1.2
21 |
22 | ### Patch Changes
23 |
24 | - Updated dependencies [b31f88c]
25 | - pinorama-server@0.1.2
26 | - pinorama-transport@0.1.2
27 |
28 | ## 0.1.1
29 |
30 | ### Patch Changes
31 |
32 | - Updated dependencies [ed89795]
33 | - pinorama-transport@0.1.1
34 | - pinorama-server@0.1.1
35 |
--------------------------------------------------------------------------------
/packages/pinorama-server/src/routes/persist.mts:
--------------------------------------------------------------------------------
1 | import { persistToFile } from "@orama/plugin-data-persistence/server"
2 | import type { FastifyInstance } from "fastify"
3 |
4 | export async function persistRoute(fastify: FastifyInstance) {
5 | fastify.route({
6 | url: "/persist",
7 | method: "post",
8 | handler: async (req, res) => {
9 | try {
10 | await persistToFile(
11 | fastify.pinoramaDb,
12 | fastify.pinoramaOpts.dbFormat,
13 | fastify.pinoramaOpts.dbPath
14 | )
15 | res.code(204).send()
16 | } catch (e) {
17 | req.log.error(e)
18 | res.code(500).send({ error: "failed to persist data" })
19 | }
20 | }
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/packages/pinorama-server/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # pinorama-server
2 |
3 | ## 0.2.1
4 |
5 | ### Patch Changes
6 |
7 | - 4980b4b: add preset feature
8 | - Updated dependencies [4980b4b]
9 | - Updated dependencies [a0495f4]
10 | - pinorama-presets@0.1.0
11 | - pinorama-types@0.1.0
12 |
13 | ## 0.2.0
14 |
15 | ### Minor Changes
16 |
17 | - 306a952: add live mode feature
18 |
19 | ### Patch Changes
20 |
21 | - f79e807: chore(server): optimize introspection route and improve code structure
22 |
23 | ## 0.1.2
24 |
25 | ### Patch Changes
26 |
27 | - b31f88c: execute build step on github release workflow
28 |
29 | ## 0.1.1
30 |
31 | ### Patch Changes
32 |
33 | - ed89795: Add repository, bugs, homepage, and author info in `package.json` file
34 |
--------------------------------------------------------------------------------
/packages/pinorama-server/src/routes/bulk.mts:
--------------------------------------------------------------------------------
1 | import { insertMultiple } from "@orama/orama"
2 | import type { FastifyInstance } from "fastify"
3 | import { withPinoramaMetadataValue } from "../utils/metadata.mjs"
4 |
5 | export async function bulkRoute(fastify: FastifyInstance) {
6 | fastify.route({
7 | url: "/bulk",
8 | method: "post",
9 | handler: async (req, res) => {
10 | try {
11 | await insertMultiple(
12 | fastify.pinoramaDb,
13 | (req.body as any).map(withPinoramaMetadataValue)
14 | )
15 | res.code(201).send({ success: true })
16 | } catch (e) {
17 | req.log.error(e)
18 | res.code(500).send({ error: "failed to insert data" })
19 | }
20 | }
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/packages/pinorama-transport/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "esModuleInterop": true,
5 | "skipLibCheck": true,
6 | "target": "es2022",
7 | "allowJs": true,
8 | "resolveJsonModule": true,
9 | "moduleDetection": "force",
10 | "isolatedModules": true,
11 | "strict": true,
12 | "noUncheckedIndexedAccess": true,
13 | "moduleResolution": "NodeNext",
14 | "module": "NodeNext",
15 | "sourceMap": true,
16 | "declaration": true,
17 | "declarationMap": true,
18 | "lib": ["ES2022"],
19 | "outDir": "dist",
20 | "types": ["vitest/globals"]
21 | },
22 | "include": ["src"],
23 | "exclude": ["node_modules", "dist", "tests"]
24 | }
25 |
--------------------------------------------------------------------------------
/.github/workflows/turbo.yml:
--------------------------------------------------------------------------------
1 | name: Turbo CI
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | pull_request:
7 | types: [opened, synchronize]
8 |
9 | jobs:
10 | build_and_test:
11 | name: Build and Test
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v4
16 |
17 | - name: Setup PNPM
18 | uses: pnpm/action-setup@v3
19 |
20 | - name: Setup Node.js
21 | uses: actions/setup-node@v4
22 | with:
23 | node-version: 20
24 | cache: "pnpm"
25 |
26 | - name: Install
27 | run: pnpm install --no-frozen-lockfile
28 |
29 | - name: Build
30 | run: pnpm build
31 |
32 | - name: Test
33 | run: pnpm test
34 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/error-state/error-state.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 | import { FormattedMessage } from "react-intl"
3 |
4 | type ErrorStateProps = {
5 | error: Error
6 | className?: string
7 | }
8 |
9 | export function ErrorState(props: ErrorStateProps) {
10 | return (
11 |
17 |
18 |
19 |
20 |
21 |
22 |
{props.error.message}
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/packages/pinorama-server/src/routes/search.mts:
--------------------------------------------------------------------------------
1 | import { type AnyOrama, type Results, search } from "@orama/orama"
2 | import type { FastifyInstance } from "fastify"
3 | import type { PinoramaDocument } from "pinorama-types"
4 |
5 | export async function searchRoute(fastify: FastifyInstance) {
6 | fastify.route({
7 | url: "/search",
8 | method: "post",
9 | handler: async (req, res) => {
10 | try {
11 | const result: Results> = await search(
12 | fastify.pinoramaDb,
13 | req.body as any
14 | )
15 | res.code(200).send(result)
16 | } catch (e) {
17 | req.log.error(e)
18 | res.code(500).send({ error: "failed to search data" })
19 | }
20 | }
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/packages/pinorama-server/src/routes/styles.mts:
--------------------------------------------------------------------------------
1 | import type { FastifyInstance } from "fastify"
2 | import type { IntrospectionStyle } from "pinorama-types"
3 | import { generateCSS } from "../utils/styles.mjs"
4 |
5 | const CSS_MIME_TYPE = "text/css"
6 |
7 | export async function stylesRoute(fastify: FastifyInstance) {
8 | const styles = fastify.pinoramaOpts?.introspection?.styles as
9 | | Record
10 | | undefined
11 |
12 | let css = ""
13 | if (styles) {
14 | css = generateCSS(styles)
15 | }
16 |
17 | fastify.route({
18 | url: "/styles.css",
19 | method: "get",
20 | handler: (req, res) => {
21 | if (!css) {
22 | req.log.warn("no styles found")
23 | }
24 | res.type(CSS_MIME_TYPE).send(css)
25 | }
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 | "baseUrl": ".",
9 | "paths": {
10 | "@/*": ["./src/*"]
11 | },
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [
25 | {
26 | "path": "./tsconfig.node.json"
27 | }
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as LabelPrimitive from "@radix-ui/react-label"
2 | import { type VariantProps, cva } from "class-variance-authority"
3 | import * as React from "react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const labelVariants = cva(
8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9 | )
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ))
22 | Label.displayName = LabelPrimitive.Root.displayName
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/title-bar/components/settings-button.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import {
3 | Tooltip,
4 | TooltipContent,
5 | TooltipPortal,
6 | TooltipTrigger
7 | } from "@/components/ui/tooltip"
8 | import { SettingsIcon } from "lucide-react"
9 |
10 | export function SettingsButton() {
11 | const handleClick = () => {}
12 |
13 | return (
14 |
15 |
16 |
24 |
25 |
26 | Settings
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/packages/pinorama-docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pinorama-docs",
3 | "version": "0.2.0",
4 | "private": true,
5 | "description": "Documentation for Pinorama",
6 | "type": "module",
7 | "scripts": {
8 | "dev": "vitepress dev",
9 | "build": "vitepress build",
10 | "preview": "vitepress preview",
11 | "clean": "rimraf .vitepress/cache .vitepress/dist node_modules"
12 | },
13 | "homepage": "https://github.com/pinoramajs/pinorama#readme",
14 | "bugs": {
15 | "url": "https://github.com/pinoramajs/pinorama/issues"
16 | },
17 | "license": "MIT",
18 | "author": "Francesco Pasqua (https://cesco.me)",
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/pinoramajs/pinorama.git",
22 | "directory": "packages/pinorama-docs"
23 | },
24 | "devDependencies": {
25 | "rimraf": "^5.0.7",
26 | "vitepress": "^1.2.3"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/index.ts:
--------------------------------------------------------------------------------
1 | import { createModule } from "@/lib/modules"
2 | import { LogExplorer } from "./components/log-explorer"
3 |
4 | export default createModule({
5 | id: "logExplorer",
6 | component: LogExplorer,
7 | routePath: "/",
8 | messages: {
9 | en: () => import("./messages/en.json"),
10 | it: () => import("./messages/it.json")
11 | },
12 | hotkeys: {
13 | focusSearch: "/",
14 | showFilters: "f",
15 | showDetails: "o",
16 | maximizeDetails: "m",
17 | liveMode: "l",
18 | refresh: "r",
19 | clearFilters: "x",
20 | selectNextRow: "j, down",
21 | selectPreviousRow: "k, up",
22 | copyToClipboard: "y, c",
23 | incrementFiltersSize: "shift+f",
24 | decrementFiltersSize: "shift+d",
25 | incrementDetailsSize: "shift+j",
26 | decrementDetailsSize: "shift+k",
27 | clearSelection: "esc"
28 | }
29 | } as const)
30 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
2 | import * as React from "react"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Separator = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(
10 | (
11 | { className, orientation = "horizontal", decorative = true, ...props },
12 | ref
13 | ) => (
14 |
25 | )
26 | )
27 | Separator.displayName = SeparatorPrimitive.Root.displayName
28 |
29 | export { Separator }
30 |
--------------------------------------------------------------------------------
/packages/pinorama-types/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pinorama-types",
3 | "version": "0.1.0",
4 | "type": "module",
5 | "types": "./dist/index.d.ts",
6 | "exports": "./dist/index.js",
7 | "files": ["dist"],
8 | "scripts": {
9 | "clean": "rimraf dist node_modules",
10 | "build": "tsc"
11 | },
12 | "homepage": "https://github.com/pinoramajs/pinorama#readme",
13 | "bugs": {
14 | "url": "https://github.com/pinoramajs/pinorama/issues"
15 | },
16 | "license": "MIT",
17 | "author": "Francesco Pasqua (https://cesco.me)",
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/pinoramajs/pinorama.git",
21 | "directory": "packages/pinorama-types"
22 | },
23 | "devDependencies": {
24 | "@orama/orama": "^3.0.4",
25 | "@types/node": "^20.14.2",
26 | "rimraf": "^5.0.7",
27 | "typescript": "^5.4.5"
28 | },
29 | "publishConfig": {
30 | "access": "public"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "Pinorama",
3 | "projectOwner": "pinoramajs",
4 | "repoType": "github",
5 | "commitConvention": "angular",
6 | "contributors": [
7 | {
8 | "login": "cesconix",
9 | "name": "Francesco Pasqua",
10 | "avatar_url": "https://avatars.githubusercontent.com/u/244004?v=4",
11 | "profile": "https://cesco.me/",
12 | "contributions": [
13 | "code",
14 | "design",
15 | "doc",
16 | "ideas",
17 | "maintenance",
18 | "projectManagement",
19 | "review"
20 | ]
21 | },
22 | {
23 | "login": "ilteoood",
24 | "name": "Matteo Pietro Dazzi",
25 | "avatar_url": "https://avatars.githubusercontent.com/u/6383527?v=4",
26 | "profile": "https://ilteoood.xyz/",
27 | "contributions": ["code", "review", "design"]
28 | }
29 | ],
30 | "files": ["README.md"],
31 | "commitType": "docs",
32 | "contributorsPerLine": 7
33 | }
34 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pinorama Studio
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/hooks/use-pinorama-styles.ts:
--------------------------------------------------------------------------------
1 | import { usePinoramaClient } from "@/contexts"
2 | import { useQuery } from "@tanstack/react-query"
3 |
4 | const PINORAMA_STYLES_ID = "pinorama-styles"
5 |
6 | const insertStyle = (data?: string) => {
7 | if (!data) return
8 |
9 | let styleElement = document.getElementById(
10 | PINORAMA_STYLES_ID
11 | ) as HTMLStyleElement
12 |
13 | if (!styleElement) {
14 | styleElement = document.createElement("style")
15 | styleElement.id = PINORAMA_STYLES_ID
16 | document.head.appendChild(styleElement)
17 | }
18 |
19 | styleElement.textContent = data
20 | }
21 |
22 | export const usePinoramaStyles = () => {
23 | const client = usePinoramaClient()
24 |
25 | return useQuery({
26 | queryKey: ["styles"],
27 | queryFn: async () => {
28 | const response = await client?.styles()
29 | insertStyle(response)
30 | return response
31 | },
32 | staleTime: Number.POSITIVE_INFINITY
33 | })
34 | }
35 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/i18n/messages/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "app.name": "Pinorama",
3 | "app.version": "v{version}",
4 | "theme.dark": "Dark",
5 | "theme.light": "Light",
6 | "theme.system": "System",
7 | "connection.labels.serverUrl": "Server URL",
8 | "connection.labels.reset": "Reset",
9 | "connection.labels.save": "Save",
10 | "connection.labels.connect": "Connect",
11 | "connection.labels.disconnect": "Disconnect",
12 | "connection.status.disconnected": "Disconnected",
13 | "connection.status.connecting": "Connecting...",
14 | "connection.status.connected": "Connected",
15 | "connection.status.failed": "Connection failed",
16 | "connection.status.unknown": "Unknown",
17 | "labels.inlineError": "Error:",
18 | "labels.loading": "Loading...",
19 | "labels.noResultFound": "No result found",
20 | "labels.copyToClipboard": "Copy to clipboard",
21 | "labels.keyboardShortcuts": "Keyboard Shortcuts",
22 | "labels.allShortcutsSeparatedByModule": "All shortcuts are separated by module"
23 | }
24 |
--------------------------------------------------------------------------------
/packages/pinorama-presets/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pinorama-presets",
3 | "version": "0.1.0",
4 | "type": "module",
5 | "types": "./dist/presets/index.d.mts",
6 | "exports": "./dist/presets/index.mjs",
7 | "files": ["dist"],
8 | "scripts": {
9 | "clean": "rimraf dist node_modules",
10 | "build": "tsc"
11 | },
12 | "homepage": "https://github.com/pinoramajs/pinorama#readme",
13 | "bugs": {
14 | "url": "https://github.com/pinoramajs/pinorama/issues"
15 | },
16 | "license": "MIT",
17 | "author": "Francesco Pasqua (https://cesco.me)",
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/pinoramajs/pinorama.git",
21 | "directory": "packages/pinorama-presets"
22 | },
23 | "devDependencies": {
24 | "@orama/orama": "^3.0.4",
25 | "@types/node": "^20.14.2",
26 | "pinorama-types": "workspace:*",
27 | "rimraf": "^5.0.7",
28 | "typescript": "^5.4.5"
29 | },
30 | "publishConfig": {
31 | "access": "public"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as PopoverPrimitive from "@radix-ui/react-popover"
2 | import * as React from "react"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Popover = PopoverPrimitive.Root
7 |
8 | const PopoverTrigger = PopoverPrimitive.Trigger
9 |
10 | const PopoverContent = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14 |
15 |
25 |
26 | ))
27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
28 |
29 | export { Popover, PopoverTrigger, PopoverContent }
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pinorama",
3 | "version": "0.1.0",
4 | "description": "simple, log search for pino",
5 | "license": "MIT",
6 | "author": {
7 | "name": "Francesco Pasqua",
8 | "email": "cesconix@me.com",
9 | "url": "https://github.com/cesconix",
10 | "author": true
11 | },
12 | "scripts": {
13 | "test": "turbo test",
14 | "clean": "turbo clean && rimraf node_modules .turbo",
15 | "build": "turbo build",
16 | "dev": "turbo dev",
17 | "biome": "biome check --no-errors-on-unmatched --files-ignore-unknown=true",
18 | "changeset": "changeset",
19 | "release": "changeset publish",
20 | "prepare": "husky"
21 | },
22 | "devDependencies": {
23 | "@biomejs/biome": "^1.9.4",
24 | "@changesets/cli": "^2.27.11",
25 | "@commitlint/cli": "^19.6.1",
26 | "@commitlint/config-conventional": "^19.6.0",
27 | "husky": "^9.1.7",
28 | "rimraf": "^5.0.10",
29 | "turbo": "^2.3.3"
30 | },
31 | "packageManager": "pnpm@9.2.0",
32 | "engines": {
33 | "node": ">=20"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-viewer/hooks/use-static-logs.ts:
--------------------------------------------------------------------------------
1 | import { usePinoramaClient } from "@/contexts"
2 | import { buildPayload } from "@/modules/log-explorer/utils"
3 | import { useQuery } from "@tanstack/react-query"
4 |
5 | import type { SearchFilters } from "../../log-filters/types"
6 |
7 | export const useStaticLogs = (
8 | searchText?: string,
9 | searchFilters?: SearchFilters,
10 | enabled?: boolean
11 | ) => {
12 | const client = usePinoramaClient()
13 |
14 | return useQuery({
15 | queryKey: ["static-logs", searchText, searchFilters],
16 | queryFn: async ({ signal }) => {
17 | await new Promise((resolve) => setTimeout(resolve, 300))
18 | if (signal.aborted) return
19 |
20 | const payload = buildPayload(searchText, searchFilters)
21 |
22 | const response = await client?.search(payload)
23 | const newData = response?.hits.map((hit) => hit.document) ?? []
24 |
25 | return newData
26 | },
27 | staleTime: Number.POSITIVE_INFINITY,
28 | enabled
29 | })
30 | }
31 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/i18n/messages/it.json:
--------------------------------------------------------------------------------
1 | {
2 | "app.name": "Pinorama",
3 | "app.version": "v{version}",
4 | "theme.dark": "Scuro",
5 | "theme.light": "Chiaro",
6 | "theme.system": "Sistema",
7 | "connection.labels.serverUrl": "URL del server",
8 | "connection.labels.reset": "Reimposta",
9 | "connection.labels.save": "Salva",
10 | "connection.labels.connect": "Connetti",
11 | "connection.labels.disconnect": "Disconnetti",
12 | "connection.status.disconnected": "Disconnesso",
13 | "connection.status.connecting": "Connessione in corso...",
14 | "connection.status.connected": "Connesso",
15 | "connection.status.failed": "Connessione fallita",
16 | "connection.status.unknown": "Sconosciuto",
17 | "labels.inlineError": "Errore:",
18 | "labels.loading": "Caricamento in corso...",
19 | "labels.noResultFound": "Nessun risultato trovato",
20 | "labels.copyToClipboard": "Copia negli appunti",
21 | "labels.keyboardShortcuts": "Scorciatoie da tastiera",
22 | "labels.allShortcutsSeparatedByModule": "Tutte le scorciatoie sono separate per modulo"
23 | }
24 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-details/styles/json-viewer.css:
--------------------------------------------------------------------------------
1 | .json-view-container {
2 | @apply whitespace-pre-wrap font-mono text-sm pl-1;
3 | }
4 |
5 | .punctuation {
6 | @apply text-muted-foreground;
7 | }
8 |
9 | .expander {
10 | @apply mr-1;
11 | }
12 |
13 | .expand-icon,
14 | .collapse-icon {
15 | @apply inline-flex w-[14px] ml-[-14px] text-lg leading-none text-muted-foreground/40;
16 | }
17 |
18 | .expand-icon::after {
19 | content: "▸";
20 | }
21 |
22 | .collapse-icon::after {
23 | content: "▾";
24 | }
25 |
26 | .collapsed-content {
27 | @apply mx-1;
28 | }
29 |
30 | .collapsed-content::after {
31 | content: "...";
32 | }
33 |
34 | .basic-element-style {
35 | @apply my-1 mx-4 p-0;
36 | }
37 |
38 | .label,
39 | .clickable-label {
40 | @apply mr-2 font-semibold;
41 | }
42 |
43 | .value-string {
44 | @apply text-sky-400;
45 | }
46 |
47 | .value-number,
48 | .value-boolean,
49 | .value-null,
50 | .value-undefined,
51 | .value-other {
52 | @apply text-rose-400;
53 | }
54 |
55 | /* .collapse-icon {} */
56 | /* .expand-icon {} */
57 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
2 | import * as React from "react"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const TooltipProvider = TooltipPrimitive.Provider
7 |
8 | const Tooltip = TooltipPrimitive.Root
9 |
10 | const TooltipTrigger = TooltipPrimitive.Trigger
11 |
12 | const TooltipPortal = TooltipPrimitive.Portal
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export {
31 | Tooltip,
32 | TooltipTrigger,
33 | TooltipPortal,
34 | TooltipContent,
35 | TooltipProvider
36 | }
37 |
--------------------------------------------------------------------------------
/examples/fastify-example/index.mjs:
--------------------------------------------------------------------------------
1 | import path from "node:path"
2 | import fastifyCors from "@fastify/cors"
3 | import Fastify from "fastify"
4 | import * as pinoramaPresets from "pinorama-presets"
5 | import { fastifyPinoramaServer } from "pinorama-server"
6 |
7 | const fastify = Fastify({
8 | logger: {
9 | transport: {
10 | targets: [
11 | {
12 | target: "pinorama-transport",
13 | options: {
14 | url: "http://localhost:6200/my_pinorama_server"
15 | }
16 | },
17 | { target: "@fastify/one-line-logger" }
18 | ]
19 | }
20 | }
21 | })
22 |
23 | fastify.register(fastifyCors)
24 |
25 | fastify.register(fastifyPinoramaServer, {
26 | prefix: "/my_pinorama_server",
27 | dbPath: path.resolve("./db.msp"),
28 | dbSchema: pinoramaPresets.fastify.schema,
29 | introspection: pinoramaPresets.fastify.introspection,
30 | logLevel: "silent" // need to avoid loop
31 | })
32 |
33 | fastify.post("/docs", async function handler(req) {
34 | req.log.info(req.body.message)
35 | return req.body.message
36 | })
37 |
38 | fastify.listen({ port: 6200 }, (err) => {
39 | if (err) throw err
40 | })
41 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/title-bar/title-bar.tsx:
--------------------------------------------------------------------------------
1 | import { ConnectionStatusButton } from "./components/connection-status-button"
2 | import { ConnectionToggleButton } from "./components/connection-toggle-button"
3 | import { HotkeysButton } from "./components/hotkeys-button"
4 | import { PinoramaLogo } from "./components/pinorama-logo"
5 | import { ThemeToggleButton } from "./components/theme-toggle-button"
6 |
7 | export function TitleBar() {
8 | return (
9 |
10 |
11 | {/* Left */}
12 |
15 |
16 | {/* Center */}
17 |
18 |
19 |
20 |
21 | {/* Right */}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/packages/pinorama-server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pinorama-server",
3 | "version": "0.2.1",
4 | "license": "MIT",
5 | "type": "module",
6 | "types": "./dist/index.d.mts",
7 | "exports": "./dist/index.mjs",
8 | "files": ["dist"],
9 | "scripts": {
10 | "clean": "rimraf dist node_modules",
11 | "build": "tsc"
12 | },
13 | "homepage": "https://github.com/pinoramajs/pinorama#readme",
14 | "bugs": {
15 | "url": "https://github.com/pinoramajs/pinorama/issues"
16 | },
17 | "author": "Francesco Pasqua (https://cesco.me)",
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/pinoramajs/pinorama.git",
21 | "directory": "packages/pinorama-server"
22 | },
23 | "devDependencies": {
24 | "@types/node": "^20.12.7",
25 | "rimraf": "^5.0.7",
26 | "typescript": "^5.4.5"
27 | },
28 | "dependencies": {
29 | "@orama/orama": "^3.0.4",
30 | "@orama/plugin-data-persistence": "3.0.4",
31 | "change-case": "^5.4.4",
32 | "fastify": "^5.2.1",
33 | "fastify-plugin": "^5.0.1",
34 | "pinorama-presets": "workspace:*",
35 | "pinorama-types": "workspace:*"
36 | },
37 | "publishConfig": {
38 | "access": "public"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
2 | import { Check } from "lucide-react"
3 | import * as React from "react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Checkbox = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => (
11 |
19 |
22 |
23 |
24 |
25 | ))
26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
27 |
28 | export { Checkbox }
29 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import modules from "@/modules"
2 |
3 | const appMessages: ImportMessages = {
4 | en: () => import("./messages/en.json"),
5 | it: () => import("./messages/it.json")
6 | }
7 |
8 | export type Locale = "en" | "it"
9 |
10 | export type Messages = Record
11 |
12 | export type ImportMessages = {
13 | [key in Locale]: () => Promise<{
14 | default: Messages
15 | }>
16 | }
17 |
18 | export const getMessages = async (locale: Locale) => {
19 | let messages: Messages = {}
20 |
21 | // App Messages
22 | try {
23 | const module = await appMessages[locale]()
24 | messages = module.default
25 | } catch (error) {
26 | console.warn(`i18n: could not load app messages for "${locale}"`)
27 | }
28 |
29 | // Module Messages
30 | for (const mod of modules) {
31 | const translationImport = mod.messages?.[locale]
32 | if (translationImport) {
33 | try {
34 | const module = await translationImport()
35 | messages = { ...messages, ...module.default }
36 | } catch (error) {
37 | console.warn(
38 | `i18n: could not load "${mod.id}" messages for "${locale}"`
39 | )
40 | }
41 | }
42 | }
43 |
44 | return messages
45 | }
46 |
--------------------------------------------------------------------------------
/packages/pinorama-server/src/utils/styles.mts:
--------------------------------------------------------------------------------
1 | import { kebabCase } from "change-case"
2 | import type { IntrospectionStyle } from "pinorama-types"
3 |
4 | type CSSProperties = Record
5 |
6 | export function generateCSS(styles: Record) {
7 | return Object.entries(styles).map(generateCSSForField).join("")
8 | }
9 |
10 | const createFieldCss = (className: string, style: CSSProperties) => {
11 | return `.${className} {
12 | ${generateCSSProps(style)}
13 | }
14 | `
15 | }
16 |
17 | const generateCSSForField = ([field, style]: [string, IntrospectionStyle]) => {
18 | const className = `pinorama-${kebabCase(field)}`
19 |
20 | if (!Array.isArray(style)) {
21 | return createFieldCss(className, style)
22 | }
23 |
24 | const [baseStyles, valueStyles] = style
25 |
26 | let fieldCSS = createFieldCss(className, baseStyles)
27 |
28 | for (const [value, valueStyle] of Object.entries(valueStyles)) {
29 | fieldCSS += createFieldCss(`${className}-${kebabCase(value)}`, valueStyle)
30 | }
31 |
32 | return fieldCSS
33 | }
34 |
35 | const generateCSSProps = (style: CSSProperties = {}) => {
36 | return Object.entries(style)
37 | .map(([cssProp, value]) => `${kebabCase(cssProp)}: ${value};`)
38 | .join("")
39 | }
40 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/title-bar/components/theme-toggle-button.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import {
3 | Tooltip,
4 | TooltipContent,
5 | TooltipPortal,
6 | TooltipTrigger
7 | } from "@/components/ui/tooltip"
8 | import { Theme, useTheme } from "@/contexts"
9 | import { MoonStarIcon, SunIcon } from "lucide-react"
10 | import { FormattedMessage, useIntl } from "react-intl"
11 |
12 | export function ThemeToggleButton() {
13 | const intl = useIntl()
14 | const { theme, setTheme } = useTheme()
15 |
16 | const handleClick = () => {
17 | setTheme(theme === Theme.Light ? Theme.Dark : Theme.Light)
18 | }
19 |
20 | const Icon = theme === Theme.Dark ? MoonStarIcon : SunIcon
21 |
22 | return (
23 |
24 |
25 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import { type VariantProps, cva } from "class-variance-authority"
2 | import type * as React from "react"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground"
18 | }
19 | },
20 | defaultVariants: {
21 | variant: "default"
22 | }
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/packages/pinorama-docs/.vitepress/style/vars.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Colors
3 | * -------------------------------------------------------------------------- */
4 |
5 | :root {
6 | --vp-c-brand-1: #59b9ff;
7 | --vp-c-brand-2: #0a7aff;
8 | --vp-c-brand-3: #0085ff;
9 | }
10 |
11 | .dark {
12 | --vp-c-brand-1: #59b9ff;
13 | --vp-c-brand-2: #0a7aff;
14 | --vp-c-brand-3: #0085ff;
15 | }
16 |
17 | /**
18 | * Component: Button
19 | * -------------------------------------------------------------------------- */
20 | .dark {
21 | --vp-button-brand-text: #ffffff;
22 | --vp-button-brand-active-text: #ffffff;
23 | --vp-button-brand-hover-text: #ffffff;
24 | }
25 |
26 | /**
27 | * Component: Home
28 | * -------------------------------------------------------------------------- */
29 |
30 | :root {
31 | --vp-home-hero-name-color: transparent;
32 | --vp-home-hero-name-background: -webkit-linear-gradient(
33 | 120deg,
34 | #0085ff 30%,
35 | #59b9ff
36 | );
37 | --vp-home-hero-image-background-image: linear-gradient(
38 | -45deg,
39 | #0085ff60 50%,
40 | #59b9ff60 50%
41 | );
42 | --vp-home-hero-image-filter: blur(30px);
43 | }
44 |
45 | @media (min-width: 640px) {
46 | :root {
47 | --vp-home-hero-image-filter: blur(56px);
48 | }
49 | }
50 |
51 | @media (min-width: 960px) {
52 | :root {
53 | --vp-home-hero-image-filter: blur(72px);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-filters/index.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react"
2 |
3 | import { Facet } from "./components/facet"
4 |
5 | import type { AnySchema } from "@orama/orama"
6 | import type { PinoramaIntrospection } from "pinorama-types"
7 | import { getFacetsConfig } from "./lib/utils"
8 | import type { SearchFilters } from "./types"
9 |
10 | type PinoramaFacetsProps = {
11 | introspection: PinoramaIntrospection
12 | searchText: string
13 | filters: SearchFilters
14 | liveMode: boolean
15 | onFiltersChange: (filters: SearchFilters) => void
16 | }
17 |
18 | export function LogFilters(props: PinoramaFacetsProps) {
19 | const facetsConfig = useMemo(() => {
20 | return getFacetsConfig(props.introspection)
21 | }, [props.introspection])
22 |
23 | return (
24 |
25 | {facetsConfig.definition.map((facet) => {
26 | return (
27 |
37 | )
38 | })}
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Docs
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - "packages/pinorama-docs/**"
8 | workflow_dispatch:
9 |
10 | permissions:
11 | contents: read
12 | pages: write
13 | id-token: write
14 |
15 | concurrency:
16 | group: pages
17 | cancel-in-progress: false
18 |
19 | jobs:
20 | build:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v4
25 |
26 | - name: Setup PNPM
27 | uses: pnpm/action-setup@v3
28 |
29 | - name: Setup Node.js
30 | uses: actions/setup-node@v4
31 | with:
32 | node-version: 20
33 | cache: pnpm
34 |
35 | - name: Setup Pages
36 | uses: actions/configure-pages@v4
37 |
38 | - name: Install
39 | run: pnpm install --frozen-lockfile
40 |
41 | - name: Build
42 | run: pnpm --filter pinorama-docs build
43 |
44 | - name: Upload Artifact
45 | uses: actions/upload-pages-artifact@v3
46 | with:
47 | path: packages/pinorama-docs/.vitepress/dist
48 |
49 | deploy:
50 | environment:
51 | name: github-pages
52 | url: ${{ steps.deployment.outputs.page_url }}
53 | needs: build
54 | runs-on: ubuntu-latest
55 | steps:
56 | - name: Deploy to GitHub Pages
57 | id: deployment
58 | uses: actions/deploy-pages@v4
59 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | concurrency: ${{ github.workflow }}-${{ github.ref }}
9 |
10 | jobs:
11 | release:
12 | name: Release
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v4
17 |
18 | - name: Setup PNPM
19 | uses: pnpm/action-setup@v3
20 |
21 | - name: Setup Node.js
22 | uses: actions/setup-node@v4
23 | with:
24 | node-version: 20
25 |
26 | - name: Install
27 | run: pnpm install --no-frozen-lockfile
28 |
29 | - name: Build
30 | run: pnpm build
31 |
32 | - name: Creating .npmrc and remove .husky
33 | run: |
34 | cat << EOF > "$HOME/.npmrc"
35 | enable-pre-post-scripts=true
36 | package-manager-strict=false
37 | //registry.npmjs.org/:_authToken=$NPM_TOKEN
38 | EOF
39 | rm -rf .husky
40 | env:
41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
42 |
43 | - name: Create Release PR or Publish to NPM
44 | id: changesets
45 | uses: changesets/action@v1
46 | with:
47 | publish: pnpm release
48 | commit: "chore: release"
49 | title: "chore: release candidate"
50 | env:
51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
53 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-filters/hooks/use-facet.ts:
--------------------------------------------------------------------------------
1 | import { usePinoramaClient } from "@/contexts"
2 | import { keepPreviousData, useQuery } from "@tanstack/react-query"
3 |
4 | import type { SearchFilters } from "../types"
5 |
6 | type OramaFacetValue = {
7 | count: number
8 | values: Record
9 | }
10 |
11 | const POLL_DELAY = 1500
12 |
13 | export const useFacet = (
14 | name: string,
15 | searchText: string,
16 | filters: SearchFilters,
17 | liveMode: boolean
18 | ) => {
19 | const client = usePinoramaClient()
20 |
21 | const query = useQuery({
22 | queryKey: ["facets", name, searchText, filters],
23 | queryFn: async ({ signal }) => {
24 | await new Promise((resolve) => setTimeout(resolve, 500))
25 |
26 | if (signal.aborted) {
27 | return
28 | }
29 |
30 | const payload: any = {
31 | preflight: true,
32 | facets: { [name]: {} }
33 | }
34 |
35 | if (searchText) {
36 | payload.term = searchText
37 | // payload.properties = [name]
38 | }
39 |
40 | if (filters) {
41 | payload.where = filters
42 | }
43 |
44 | const response: any = await client?.search(payload)
45 | return response.facets[name]
46 | },
47 | placeholderData: keepPreviousData,
48 | refetchInterval: liveMode ? POLL_DELAY : false
49 | })
50 |
51 | return query
52 | }
53 |
--------------------------------------------------------------------------------
/examples/create-server-example/index.mjs:
--------------------------------------------------------------------------------
1 | import path from "node:path"
2 | import fastifyCors from "@fastify/cors"
3 | import Fastify from "fastify"
4 | import { createServer } from "pinorama-server"
5 |
6 | const pinoramaServer = createServer(
7 | {
8 | // prefix: "/my_pinorama_server",
9 | dbPath: path.resolve("./db.msp")
10 | },
11 | {
12 | logger: {
13 | transport: {
14 | targets: [
15 | { target: "@fastify/one-line-logger", options: { colorize: true } }
16 | ]
17 | }
18 | }
19 | }
20 | )
21 |
22 | pinoramaServer.register(fastifyCors)
23 |
24 | pinoramaServer.listen({ port: 6200 }, (err, address) => {
25 | if (err) throw err
26 | console.log(`Pinorama server listening at ${address}`)
27 | })
28 |
29 | const genericServer = Fastify({
30 | logger: {
31 | transport: {
32 | targets: [
33 | {
34 | target: "pinorama-transport",
35 | options: {
36 | // url: "http://localhost:6200/my_pinorama_server"
37 | url: "http://localhost:6200"
38 | }
39 | },
40 | { target: "@fastify/one-line-logger" }
41 | ]
42 | }
43 | }
44 | })
45 |
46 | genericServer.post("/docs", async function handler(req) {
47 | req.log.info(req.body.message)
48 | return req.body.message
49 | })
50 |
51 | genericServer.listen({ port: 3000 }, (err, address) => {
52 | if (err) throw err
53 | console.log(`Generic server listening at ${address}`)
54 | })
55 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-filters/components/facet-factory-input.tsx:
--------------------------------------------------------------------------------
1 | import { Checkbox } from "@/components/ui/checkbox"
2 | import { facetFilterOperationsFactory } from "../lib/operations"
3 |
4 | import type { IntrospectionFacet } from "pinorama-types"
5 | import type { SearchFilters } from "../types"
6 |
7 | export function FacetFactoryInput(props: {
8 | id: string
9 | type: IntrospectionFacet
10 | name: string
11 | value: string | number
12 | filters: SearchFilters
13 | onFiltersChange: (filters: SearchFilters) => void
14 | }) {
15 | const operations: any = facetFilterOperationsFactory(props.type)
16 |
17 | const criteria = props.filters[props.name] || operations.create()
18 | const checked = operations.exists(props.value, criteria)
19 |
20 | const handleCheckedChange = (checked: boolean) => {
21 | const newCriteria = checked
22 | ? operations.add(criteria, props.value)
23 | : operations.remove(criteria, props.value)
24 |
25 | const filters = { ...props.filters, [props.name]: newCriteria }
26 |
27 | if (operations.length(newCriteria) === 0) {
28 | delete filters[props.name]
29 | }
30 |
31 | props.onFiltersChange(filters)
32 | }
33 |
34 | return (
35 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-filters/lib/operations.ts:
--------------------------------------------------------------------------------
1 | import type { IntrospectionFacet } from "pinorama-types"
2 | import type { EnumFilter, StringFilter } from "../types"
3 |
4 | export const facetFilterOperationsFactory = (type: IntrospectionFacet) => {
5 | switch (type) {
6 | case "enum":
7 | return createEnumOperations()
8 | case "string":
9 | return createStringOperations()
10 | default:
11 | throw new Error(`unsupported type "${type}" for facet operations`)
12 | }
13 | }
14 |
15 | type FacetOperations = {
16 | create: () => C
17 | exists: (value: V, criteria: C) => boolean
18 | length: (criteria: C) => number
19 | values: (criteria?: C) => unknown[]
20 | add: (criteria: C, value: V) => C
21 | remove: (criteria: C, value: V) => C
22 | }
23 |
24 | const createEnumOperations = (): FacetOperations<
25 | EnumFilter,
26 | string | number
27 | > => ({
28 | create: () => ({ in: [] }),
29 | exists: (v, c) => c.in.includes(v),
30 | length: (c) => c.in.length,
31 | values: (c) => c?.in || [],
32 | add: (c, v) => ({ in: [...c.in, v] }),
33 | remove: (c, v) => ({ in: c.in.filter((_v) => _v !== v) })
34 | })
35 |
36 | const createStringOperations = (): FacetOperations => ({
37 | create: () => [],
38 | exists: (v, c) => c.includes(v),
39 | length: (c) => c.length,
40 | values: (c) => c || [],
41 | add: (c, v) => [...c, v],
42 | remove: (c, v) => c.filter((_v) => _v !== v)
43 | })
44 |
--------------------------------------------------------------------------------
/packages/pinorama-transport/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pinorama-transport",
3 | "version": "0.1.3",
4 | "description": "load pino logs into Pinorama",
5 | "type": "module",
6 | "types": "./dist/lib.d.mts",
7 | "exports": "./dist/lib.mjs",
8 | "bin": {
9 | "pino-pinorama": "./dist/cli.mjs"
10 | },
11 | "files": ["dist"],
12 | "scripts": {
13 | "test": "vitest run",
14 | "test:watch": "vitest",
15 | "test:debug": "vitest --inspect-brk --no-file-parallelism",
16 | "clean": "rimraf dist node_modules",
17 | "build": "tsc",
18 | "postbuild": "chmod u+x ./dist/cli.mjs"
19 | },
20 | "homepage": "https://github.com/pinoramajs/pinorama#readme",
21 | "bugs": {
22 | "url": "https://github.com/pinoramajs/pinorama/issues"
23 | },
24 | "license": "MIT",
25 | "author": "Francesco Pasqua (https://cesco.me)",
26 | "repository": {
27 | "type": "git",
28 | "url": "https://github.com/pinoramajs/pinorama.git",
29 | "directory": "packages/pinorama-transport"
30 | },
31 | "devDependencies": {
32 | "@types/minimist": "^1.2.5",
33 | "@types/node": "^20.14.2",
34 | "@vitest/coverage-v8": "^1.6.0",
35 | "pino": "^9.1.0",
36 | "pinorama-client": "workspace:*",
37 | "pinorama-server": "workspace:*",
38 | "pinorama-types": "workspace:*",
39 | "rimraf": "^5.0.7",
40 | "typescript": "^5.4.5",
41 | "vitest": "^1.6.0"
42 | },
43 | "publishConfig": {
44 | "access": "public"
45 | },
46 | "dependencies": {
47 | "minimist": "^1.2.8",
48 | "pino-abstract-transport": "^1.2.0"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/root.tsx:
--------------------------------------------------------------------------------
1 | import "./styles/globals.css"
2 |
3 | import {
4 | RouterProvider,
5 | createRootRoute,
6 | createRoute,
7 | createRouter,
8 | redirect
9 | } from "@tanstack/react-router"
10 | import { StrictMode } from "react"
11 |
12 | import { TooltipProvider } from "@/components/ui/tooltip"
13 | import {
14 | AppConfigProvider,
15 | I18nProvider,
16 | PinoramaClientProvider,
17 | ThemeProvider
18 | } from "@/contexts"
19 | import App from "./app"
20 | import modules from "./modules"
21 |
22 | const rootRoute = createRootRoute({
23 | component: App
24 | })
25 |
26 | const routes = modules.map((mod) =>
27 | createRoute({
28 | getParentRoute: () => rootRoute,
29 | path: mod.routePath,
30 | component: mod.component
31 | })
32 | )
33 |
34 | const router = createRouter({
35 | routeTree: rootRoute.addChildren(routes),
36 | defaultPreload: "intent",
37 | defaultStaleTime: 5000,
38 | notFoundRoute: createRoute({
39 | id: "not-found",
40 | getParentRoute: () => rootRoute,
41 | beforeLoad: () => redirect({ to: "/" })
42 | })
43 | })
44 |
45 | export function RootComponent() {
46 | return (
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/packages/pinorama-presets/src/presets/pino.mts:
--------------------------------------------------------------------------------
1 | import { createPreset } from "../utils.mjs"
2 |
3 | export const pino = createPreset(
4 | {
5 | time: "number",
6 | level: "enum",
7 | msg: "string",
8 | pid: "enum",
9 | hostname: "string"
10 | },
11 | {
12 | facets: {
13 | level: "enum",
14 | msg: "string",
15 | pid: "enum",
16 | hostname: "string"
17 | },
18 | columns: {
19 | time: { visible: true, size: 150 },
20 | level: { visible: true, size: 70 },
21 | msg: { visible: true, size: 400 },
22 | pid: { visible: false, size: 70 },
23 | hostname: { visible: false, size: 150 }
24 | },
25 | labels: {
26 | time: "Time",
27 | level: [
28 | "Level",
29 | {
30 | 10: "TRACE",
31 | 20: "DEBUG",
32 | 30: "INFO",
33 | 40: "WARN",
34 | 50: "ERROR",
35 | 60: "FATAL"
36 | }
37 | ],
38 | msg: "Message",
39 | pid: "PID",
40 | hostname: "Host"
41 | },
42 | formatters: {
43 | time: "timestamp"
44 | },
45 | styles: {
46 | time: {
47 | opacity: "0.5"
48 | },
49 | level: [
50 | {},
51 | {
52 | 10: { color: "var(--color-gray-500)" },
53 | 20: { color: "var(--color-purple-500)" },
54 | 30: { color: "var(--color-lime-500)" },
55 | 40: { color: "var(--color-yellow-500)" },
56 | 50: { color: "var(--color-red-500)" },
57 | 60: { color: "var(--color-red-500)" }
58 | }
59 | ]
60 | }
61 | }
62 | )
63 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/contexts/pinorama-client-context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from "react"
2 |
3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
4 | import { PinoramaClient } from "pinorama-client/browser"
5 | import type { BaseOramaPinorama } from "pinorama-types"
6 | import { useAppConfig } from "./app-config-context"
7 |
8 | type PinoramaClientProviderProps = {
9 | children: React.ReactNode
10 | }
11 |
12 | const PinoramaClientContext =
13 | createContext | null>(null)
14 |
15 | export function PinoramaClientProvider({
16 | children
17 | }: PinoramaClientProviderProps) {
18 | const appConfig = useAppConfig()
19 |
20 | const queryClient = new QueryClient({
21 | defaultOptions: {
22 | queries: {
23 | enabled: appConfig?.config.connectionIntent
24 | }
25 | }
26 | })
27 |
28 | const pinoramaClient: PinoramaClient | null = appConfig
29 | ?.config.connectionUrl
30 | ? new PinoramaClient({ url: appConfig.config.connectionUrl })
31 | : null
32 |
33 | return (
34 |
35 |
36 | {children}
37 |
38 |
39 | )
40 | }
41 |
42 | export const usePinoramaClient = () => {
43 | const context = useContext(PinoramaClientContext)
44 |
45 | if (context === undefined) {
46 | throw new Error(
47 | "usePinoramaClient must be used within a PinoramaClientProvider"
48 | )
49 | }
50 |
51 | return context
52 | }
53 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | import * as TogglePrimitive from "@radix-ui/react-toggle"
2 | import { type VariantProps, cva } from "class-variance-authority"
3 | import * as React from "react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const toggleVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-transparent",
13 | outline:
14 | "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground"
15 | },
16 | size: {
17 | default: "h-10 px-3",
18 | sm: "h-9 px-2.5",
19 | lg: "h-11 px-5"
20 | }
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | size: "default"
25 | }
26 | }
27 | )
28 |
29 | const Toggle = React.forwardRef<
30 | React.ElementRef,
31 | React.ComponentPropsWithoutRef &
32 | VariantProps
33 | >(({ className, variant, size, ...props }, ref) => (
34 |
39 | ))
40 |
41 | Toggle.displayName = TogglePrimitive.Root.displayName
42 |
43 | export { Toggle, toggleVariants }
44 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/icon-button/icon-button.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import {
3 | Tooltip,
4 | TooltipContent,
5 | TooltipPortal,
6 | TooltipTrigger
7 | } from "@/components/ui/tooltip"
8 | import { cn } from "@/lib/utils"
9 | import { LoaderIcon, type LucideIcon } from "lucide-react"
10 | import type { ComponentProps } from "react"
11 | import { Kbd } from "../kbd/kbd"
12 |
13 | type IconButtonProps = ComponentProps & {
14 | icon: LucideIcon
15 | tooltip?: string
16 | loading?: boolean
17 | keystroke?: string
18 | }
19 |
20 | export function IconButton({
21 | variant = "outline2",
22 | icon,
23 | tooltip,
24 | keystroke,
25 | loading,
26 | ...props
27 | }: IconButtonProps) {
28 | const Icon = loading ? LoaderIcon : icon
29 |
30 | const Component = (
31 |
34 | )
35 |
36 | return tooltip || keystroke
37 | ? withTooltip(Component, tooltip, keystroke)
38 | : Component
39 | }
40 |
41 | function withTooltip(
42 | WrappedComponent: React.ReactNode,
43 | tooltip?: string,
44 | keystroke?: string
45 | ) {
46 | return (
47 |
48 | {WrappedComponent}
49 |
50 |
51 | {tooltip}
52 | {keystroke ? {keystroke} : null}
53 |
54 |
55 |
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-viewer/components/thead.tsx:
--------------------------------------------------------------------------------
1 | import { type Table, flexRender } from "@tanstack/react-table"
2 |
3 | export function TableHead({ table }: { table: Table }) {
4 | return (
5 |
6 | {table.getHeaderGroups().map((headerGroup) => (
7 |
8 | {headerGroup.headers.map((header) => (
9 | |
15 | {header.isPlaceholder
16 | ? null
17 | : flexRender(
18 | header.column.columnDef.header,
19 | header.getContext()
20 | )}
21 | {header.column.getCanResize() && (
22 |
31 | )}
32 | |
33 | ))}
34 |
35 | ))}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/search-input/search-input.tsx:
--------------------------------------------------------------------------------
1 | import { SearchIcon, XIcon } from "lucide-react"
2 | import { forwardRef } from "react"
3 | import { Kbd } from "../kbd"
4 | import { Button } from "../ui/button"
5 | import { Input } from "../ui/input"
6 |
7 | type SearchInputProps = {
8 | value: string
9 | onChange: (text: string) => void
10 | placeholder: string
11 | keystroke?: string
12 | }
13 |
14 | export const SearchInput = forwardRef(function SearchInput(
15 | props: SearchInputProps,
16 | ref: React.Ref
17 | ) {
18 | const hasValue = props.value.length > 0
19 |
20 | return (
21 |
22 |
23 | props.onChange(e.target.value)}
30 | onKeyDown={(e) => {
31 | if (e.key === "Escape" && e.target instanceof HTMLInputElement) {
32 | e.target.blur()
33 | }
34 | }}
35 | />
36 | {hasValue ? (
37 |
46 | ) : null}
47 | {props.keystroke ? (
48 | {props.keystroke}
49 | ) : null}
50 |
51 | )
52 | })
53 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/messages/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "logExplorer.title": "Log Explorer",
3 | "logExplorer.searchLogs": "Search logs...",
4 | "logExplorer.noLogsFound": "No logs found. Please check filters.",
5 | "logExplorer.liveMode": "Live Mode",
6 | "logExplorer.columns": "Columns",
7 | "logExplorer.resetColumns": "Reset columns",
8 | "logExplorer.notConnected.title": "Not Connected",
9 | "logExplorer.notConnected.message": "You are currently disconnected. Please connect to view logs.",
10 | "logExplorer.notConnected.action": "Connect",
11 | "logExplorer.noDataSelected.title": "No data selected",
12 | "logExplorer.noDataSelected.message": "Select a row to view details.",
13 | "logExplorer.hotkeys.focusSearch": "Focus search bar",
14 | "logExplorer.hotkeys.showFilters": "Show or hide filters",
15 | "logExplorer.hotkeys.showDetails": "Show or hide details",
16 | "logExplorer.hotkeys.maximizeDetails": "Maximize details",
17 | "logExplorer.hotkeys.liveMode": "Enable or disable Live Mode",
18 | "logExplorer.hotkeys.refresh": "Refresh data",
19 | "logExplorer.hotkeys.clearFilters": "Clear all filters",
20 | "logExplorer.hotkeys.selectNextRow": "Select next row",
21 | "logExplorer.hotkeys.selectPreviousRow": "Select previous row",
22 | "logExplorer.hotkeys.copyToClipboard": "Copy to clipboard",
23 | "logExplorer.hotkeys.incrementFiltersSize": "Increase filters area",
24 | "logExplorer.hotkeys.decrementFiltersSize": "Decrease filters area",
25 | "logExplorer.hotkeys.incrementDetailsSize": "Increase details area",
26 | "logExplorer.hotkeys.decrementDetailsSize": "Decrease details area",
27 | "logExplorer.hotkeys.clearSelection": "Clear selection"
28 | }
29 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/clipboard-button/clipboard-button.tsx:
--------------------------------------------------------------------------------
1 | import { CheckIcon, CopyIcon } from "lucide-react"
2 | import { forwardRef, useCallback, useImperativeHandle, useState } from "react"
3 | import { useIntl } from "react-intl"
4 | import { IconButton } from "../icon-button/icon-button"
5 |
6 | type ClipboardButtonProps = {
7 | textToCopy: string
8 | keystroke?: string
9 | }
10 |
11 | export type ImperativeClipboardButtonHandle = {
12 | copyToClipboard: () => void
13 | }
14 |
15 | export const ClipboardButton = forwardRef<
16 | ImperativeClipboardButtonHandle,
17 | ClipboardButtonProps
18 | >(function ClipboardButton(props, ref) {
19 | const intl = useIntl()
20 | const [isCopied, setIsCopied] = useState(false)
21 |
22 | const handleClick = useCallback(async () => {
23 | if (isCopied || !props.textToCopy) {
24 | return
25 | }
26 |
27 | try {
28 | await navigator.clipboard.writeText(props.textToCopy)
29 | setIsCopied(true)
30 |
31 | setTimeout(() => {
32 | setIsCopied(false)
33 | }, 1500)
34 | } catch (err) {
35 | console.error("Failed to copy to clipboard", err)
36 | }
37 | }, [props.textToCopy, isCopied])
38 |
39 | useImperativeHandle(
40 | ref,
41 | () => ({
42 | copyToClipboard: handleClick
43 | }),
44 | [handleClick]
45 | )
46 |
47 | return (
48 |
57 | )
58 | })
59 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 240 10% 3.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 240 10% 3.9%;
13 | --primary: 240 5.9% 10%;
14 | --primary-foreground: 0 0% 98%;
15 | --secondary: 240 4.8% 95.9%;
16 | --secondary-foreground: 240 5.9% 10%;
17 | --muted: 240 4.8% 95.9%;
18 | --muted-foreground: 240 3.8% 46.1%;
19 | --accent: 240 4.8% 95.9%;
20 | --accent-foreground: 240 5.9% 10%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 0 0% 98%;
23 | --border: 240 5.9% 90%;
24 | --input: 240 5.9% 90%;
25 | --ring: 240 5.9% 10%;
26 | --radius: 0.5rem;
27 | }
28 |
29 | .dark {
30 | --background: 240 10% 3.9%;
31 | --foreground: 0 0% 98%;
32 | --card: 240 10% 3.9%;
33 | --card-foreground: 0 0% 98%;
34 | --popover: 240 10% 3.9%;
35 | --popover-foreground: 0 0% 98%;
36 | --primary: 0 0% 98%;
37 | --primary-foreground: 240 5.9% 10%;
38 | --secondary: 240 3.7% 15.9%;
39 | --secondary-foreground: 0 0% 98%;
40 | --muted: 240 3.7% 15.9%;
41 | --muted-foreground: 240 5% 64.9%;
42 | --accent: 240 3.7% 15.9%;
43 | --accent-foreground: 0 0% 98%;
44 | --destructive: 0 62.8% 30.6%;
45 | --destructive-foreground: 0 0% 98%;
46 | --border: 240 3.7% 15.9%;
47 | --input: 240 3.7% 15.9%;
48 | --ring: 240 4.9% 83.9%;
49 | }
50 | }
51 |
52 | @layer base {
53 | * {
54 | @apply border-border;
55 | }
56 |
57 | body {
58 | @apply bg-background text-foreground;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-filters/components/facet-item.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "@/components/ui/badge"
2 | import { Label } from "@/components/ui/label"
3 | import { createField } from "@/lib/introspection"
4 | import { FacetFactoryInput } from "./facet-factory-input"
5 |
6 | import type { AnySchema } from "@orama/orama"
7 | import type { IntrospectionFacet, PinoramaIntrospection } from "pinorama-types"
8 | import type { SearchFilters } from "../types"
9 |
10 | type FacetItemProps = {
11 | introspection: PinoramaIntrospection
12 | name: string
13 | type: IntrospectionFacet
14 | value: string | number
15 | count: number
16 | filters: SearchFilters
17 | onFiltersChange: (filters: SearchFilters) => void
18 | }
19 |
20 | export function FacetItem(props: FacetItemProps) {
21 | const field = createField(props.name, props.introspection)
22 |
23 | return (
24 |
25 |
33 |
39 |
43 | {props.count}
44 |
45 |
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-viewer/components/tbody.tsx:
--------------------------------------------------------------------------------
1 | import { type Row, flexRender } from "@tanstack/react-table"
2 | import type { Virtualizer } from "@tanstack/react-virtual"
3 |
4 | type TableBodyProps = {
5 | virtualizer: Virtualizer
6 | rows: Row[]
7 | }
8 |
9 | export function TableBody({ virtualizer, rows }: TableBodyProps) {
10 | return (
11 |
15 | {virtualizer.getVirtualItems().map((virtualItem) => {
16 | const row = rows[virtualItem.index] as Row
17 | const cells = row.getVisibleCells()
18 |
19 | return (
20 | {}}
25 | className={`select-none cursor-pointer flex absolute hover:bg-muted/50 odd:bg-muted/20 w-full ${row.getIsSelected() ? "bg-muted/75 hover:bg-muted/75 odd:bg-muted/75" : ""}`}
26 | style={{ transform: `translateY(${virtualItem.start}px)` }}
27 | >
28 | {cells.map((cell) => {
29 | return (
30 | |
37 | {flexRender(cell.column.columnDef.cell, cell.getContext())}
38 | |
39 | )
40 | })}
41 |
42 | )
43 | })}
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/packages/pinorama-docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: home
3 |
4 | title: Pinorama
5 | titleTemplate: Explore your logs smarter and faster
6 |
7 | hero:
8 | name: Pinorama
9 | text: Explore your logs smarter and faster
10 | tagline: A beautiful yet powerful Node.js log viewer
11 | actions:
12 | - theme: brand
13 | text: Get Started
14 | link: /guide/
15 | - theme: alt
16 | text: Pinorama Studio
17 | link: /guide/install/
18 | - theme: alt
19 | text: View on GitHub
20 | link: https://github.com/pinoramajs/pinorama
21 | image:
22 | src: "/pinorama-logo.webp"
23 | alt: Pinorama
24 |
25 | features:
26 | - title: Orama Powered
27 | icon: 🚀
28 | details: Easily filter logs by facets and perform searches with the Orama super powers.
29 | - title: Fastify Integration
30 | icon: 🔋
31 | details: Connect your Fastify app to a Pinorama and send logs with the transport plugin.
32 | - title: Piped Output
33 | icon: 📡
34 | details: Pipe Node.js logs from any process to Pinorama and view them in real-time.
35 | - title: NDJSON Support
36 | icon: 🍋
37 | details: Configure Pinorama to handle NDJSON files and display them in a structured way.
38 | - title: Custom Styles
39 | icon: 🎨
40 | details: Customize log lines with your own colors and styles to make them stand out.
41 | - title: ESM Support
42 | icon: 📦
43 | details: Built with ESM in mind, ensuring it works with modern JS standards.
44 | ---
45 |
46 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/packages/pinorama-types/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { AnyOrama, AnySchema, Orama, TypedDocument } from "@orama/orama"
2 |
3 | type SafeFlatten = T extends object
4 | ? { [K in keyof T]: SafeFlatten }
5 | : T
6 |
7 | type FlattenedKeys = T extends object
8 | ? {
9 | [K in keyof T]:
10 | | `${K & string}`
11 | | `${K & string}.${FlattenedKeys & string}`
12 | }[keyof T]
13 | : never
14 |
15 | export type PinoramaIntrospection = {
16 | facets?: Partial<{
17 | [K in FlattenedKeys>]: IntrospectionFacet
18 | }>
19 | columns: Partial<{
20 | [K in FlattenedKeys>]: IntrospectionColumn
21 | }>
22 | labels?: Partial<{
23 | [K in FlattenedKeys>]: IntrospectionLabel
24 | }>
25 | formatters?: Partial<{
26 | [K in FlattenedKeys>]: IntrospectionFormatter
27 | }>
28 | styles?: Partial<{
29 | [K in FlattenedKeys>]: IntrospectionStyle
30 | }>
31 | }
32 |
33 | export type IntrospectionFacet = "enum" | "string"
34 |
35 | export type IntrospectionColumn = {
36 | visible: boolean
37 | size?: number
38 | }
39 |
40 | export type IntrospectionLabel =
41 | | string
42 | | [string, Record]
43 |
44 | export type IntrospectionFormatter = string | [string, Record]
45 |
46 | export type IntrospectionStyle =
47 | | Record
48 | | [Record, Record>]
49 |
50 | export type PinoramaDocumentMetadata = {
51 | _pinorama: {
52 | createdAt: string
53 | }
54 | }
55 |
56 | const baseSchema = {
57 | _pinorama: {
58 | createdAt: "number"
59 | }
60 | } as const
61 |
62 | export type BaseOramaPinorama = Orama
63 | export type PinoramaDocument = TypedDocument
64 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/messages/it.json:
--------------------------------------------------------------------------------
1 | {
2 | "logExplorer.title": "Log Explorer",
3 | "logExplorer.searchLogs": "Cerca nei log...",
4 | "logExplorer.noLogsFound": "Nessun log trovato. Si prega di controllare i filtri.",
5 | "logExplorer.liveMode": "Modalità Live",
6 | "logExplorer.columns": "Colonne",
7 | "logExplorer.resetColumns": "Reimposta colonne",
8 | "logExplorer.notConnected.title": "Non Connesso",
9 | "logExplorer.notConnected.message": "Sei attualmente disconnesso. Connettiti per visualizzare i log.",
10 | "logExplorer.notConnected.action": "Connetti",
11 | "logExplorer.noDataSelected.title": "Nessun dato selezionato",
12 | "logExplorer.noDataSelected.message": "Seleziona una riga per visualizzarne i dettagli.",
13 | "logExplorer.hotkeys.focusSearch": "Focus sulla barra di ricerca",
14 | "logExplorer.hotkeys.showFilters": "Mostra o nascondi i filtri",
15 | "logExplorer.hotkeys.showDetails": "Mostra o nascondi i dettagli",
16 | "logExplorer.hotkeys.maximizeDetails": "Massimizza i dettagli",
17 | "logExplorer.hotkeys.liveMode": "Abilita o disabilita la modalità Live",
18 | "logExplorer.hotkeys.refresh": "Aggiorna i dati",
19 | "logExplorer.hotkeys.clearFilters": "Cancella tutti i filtri",
20 | "logExplorer.hotkeys.selectNextRow": "Seleziona la riga successiva",
21 | "logExplorer.hotkeys.selectPreviousRow": "Seleziona la riga precedente",
22 | "logExplorer.hotkeys.copyToClipboard": "Copia negli appunti",
23 | "logExplorer.hotkeys.incrementFiltersSize": "Aumenta l'area dei filtri",
24 | "logExplorer.hotkeys.decrementFiltersSize": "Diminuisci l'area dei filtri",
25 | "logExplorer.hotkeys.incrementDetailsSize": "Aumenta l'area dei dettagli",
26 | "logExplorer.hotkeys.decrementDetailsSize": "Diminuisci l'area dei dettagli",
27 | "logExplorer.hotkeys.clearSelection": "Cancella la selezione"
28 | }
29 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/utils/index.ts:
--------------------------------------------------------------------------------
1 | import type { SearchParams } from "@orama/orama"
2 | import type { BaseOramaPinorama } from "pinorama-types"
3 |
4 | const createBasePayload = (): SearchParams => ({
5 | limit: 10_000,
6 | sortBy: { property: "_pinorama.createdAt" }
7 | })
8 |
9 | const withSearchText =
10 | (searchText: string) =>
11 | (payload: SearchParams): SearchParams => {
12 | return { ...payload, term: searchText }
13 | }
14 |
15 | const withSearchFilters =
16 | (
17 | searchFilters: SearchParams["where"]
18 | ) =>
19 | (payload: SearchParams): SearchParams => {
20 | const where: SearchParams["where"] = payload.where || {}
21 | return { ...payload, where: { ...where, ...searchFilters } }
22 | }
23 |
24 | const withCursor =
25 | (cursor: number) =>
26 | (payload: SearchParams): SearchParams => {
27 | const where: SearchParams["where"] = payload.where || {}
28 | return {
29 | ...payload,
30 | where: { ...where, "_pinorama.createdAt": { gt: cursor || 0 } }
31 | }
32 | }
33 |
34 | export const buildPayload = (
35 | searchText?: string,
36 | searchFilters?: SearchParams["where"],
37 | cursor?: number
38 | ) => {
39 | let payload = createBasePayload()
40 |
41 | if (searchText) {
42 | const addSearchText = withSearchText(searchText)
43 | payload = addSearchText(payload)
44 | }
45 |
46 | if (searchFilters) {
47 | const addSearchFilters = withSearchFilters(searchFilters)
48 | payload = addSearchFilters(payload)
49 | }
50 |
51 | if (cursor) {
52 | const addCursor = withCursor(cursor)
53 | payload = addCursor(payload)
54 | }
55 |
56 | return payload
57 | }
58 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/lib/introspection.ts:
--------------------------------------------------------------------------------
1 | import type { AnySchema } from "@orama/orama"
2 | import { kebabCase } from "change-case"
3 | import { format } from "date-fns"
4 | import type { PinoramaIntrospection } from "pinorama-types"
5 |
6 | export function createField(
7 | fieldName: string,
8 | introspection: PinoramaIntrospection
9 | ) {
10 | const label = introspection.labels?.[fieldName]
11 | const style = introspection.styles?.[fieldName]
12 | const formatter = introspection.formatters?.[fieldName]
13 |
14 | return {
15 | getDisplayLabel() {
16 | return Array.isArray(label) ? label?.[0] : label ? label : fieldName
17 | },
18 | getValueLabel(value: string | number) {
19 | if (this.hasValueLabels()) {
20 | return label?.[1][value as number] ?? value
21 | }
22 | return value
23 | },
24 | hasValueStyle() {
25 | return Array.isArray(style)
26 | },
27 | hasValueLabels() {
28 | return Array.isArray(label) && label.length === 2
29 | },
30 | getClassName(value: string | number) {
31 | const css: string[] = []
32 |
33 | if (!style) return css
34 |
35 | const className = `pinorama-${kebabCase(fieldName)}`
36 | css.push(className)
37 |
38 | if (this.hasValueStyle()) {
39 | const [, valueStyles] = style as any
40 |
41 | const valueStyle = valueStyles[value]
42 | if (valueStyle) {
43 | css.push(`${className}-${value.toString().toLowerCase()}`)
44 | }
45 | }
46 |
47 | return css
48 | },
49 | format(value: string | number) {
50 | if (formatter === "timestamp") {
51 | return format(new Date(value), "MMM dd HH:mm:ss")
52 | }
53 |
54 | if (this.hasValueLabels()) {
55 | return this.getValueLabel(value)
56 | }
57 |
58 | return value
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/empty-state/empty-state.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 | import type { ElementType } from "react"
3 | import { Button } from "../ui/button"
4 |
5 | type EmptyStateProps = {
6 | message: string
7 | className?: string
8 | }
9 |
10 | export function EmptyStateInline(props: EmptyStateProps) {
11 | return (
12 |
18 | {props.message}
19 |
20 | )
21 | }
22 |
23 | type ButtonProps = {
24 | text: string
25 | onClick: () => void
26 | }
27 |
28 | type EmptyStateBlockProps = {
29 | icon: ElementType
30 | title: string
31 | message: string
32 | buttons?: ButtonProps[]
33 | }
34 |
35 | export function EmptyStateBlock({
36 | icon: Icon,
37 | title,
38 | message,
39 | buttons
40 | }: EmptyStateBlockProps) {
41 | return (
42 |
43 |
44 |
45 |
46 |
47 |
{title}
48 |
{message}
49 |
50 | {buttons?.map((button) => (
51 |
60 | ))}
61 |
62 |
63 |
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-filters/components/facet-header.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import { createField } from "@/lib/introspection"
3 | import type { AnySchema } from "@orama/orama"
4 | import { ChevronDown, ChevronRight, CircleX, LoaderIcon } from "lucide-react"
5 | import type { PinoramaIntrospection } from "pinorama-types"
6 | import type React from "react"
7 |
8 | type FacetHeaderProps = {
9 | introspection: PinoramaIntrospection
10 | name: string
11 | loading: boolean
12 | count: number
13 | open: boolean
14 | onClick: () => void
15 | onCountClick: (e: React.MouseEvent) => void
16 | }
17 |
18 | export function FacetHeader(props: FacetHeaderProps) {
19 | const field = createField(props.name, props.introspection)
20 | const ChevronIcon = props.open ? ChevronDown : ChevronRight
21 |
22 | return (
23 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | import { GripVertical } from "lucide-react"
2 | import * as ResizablePrimitive from "react-resizable-panels"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const ResizablePanelGroup = ({
7 | className,
8 | ...props
9 | }: React.ComponentProps) => (
10 |
17 | )
18 |
19 | const ResizablePanel = ResizablePrimitive.Panel
20 |
21 | const ResizableHandle = ({
22 | withHandle,
23 | className,
24 | ...props
25 | }: React.ComponentProps & {
26 | withHandle?: boolean
27 | }) => (
28 | div]:rotate-90",
31 | className
32 | )}
33 | {...props}
34 | >
35 | {withHandle && (
36 |
37 |
38 |
39 | )}
40 |
41 | )
42 |
43 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
44 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/contexts/i18n-context.tsx:
--------------------------------------------------------------------------------
1 | import { type Locale, type Messages, getMessages } from "@/i18n"
2 | import { useContext, useEffect, useState } from "react"
3 | import { createContext } from "react"
4 | import type React from "react"
5 | import { IntlProvider } from "react-intl"
6 |
7 | type I18nProviderProps = {
8 | children: React.ReactNode
9 | }
10 |
11 | type LocaleContextProps = {
12 | locale: Locale
13 | setLocale: (locale: Locale) => void
14 | }
15 |
16 | const LocaleContext = createContext(undefined)
17 |
18 | const defaultLocale: Locale = "en"
19 |
20 | export function I18nProvider(props: I18nProviderProps) {
21 | const [locale, setLocale] = useState(
22 | (localStorage.getItem("locale") as Locale) ?? defaultLocale
23 | )
24 | const [messages, setMessages] = useState(null)
25 |
26 | useEffect(() => {
27 | const loadMessages = async () => {
28 | const messages = await getMessages(locale)
29 | setMessages(messages)
30 | }
31 |
32 | loadMessages()
33 | }, [locale])
34 |
35 | const handleChangeLocale = async (newLocale: Locale) => {
36 | setMessages(null)
37 | const messages = await getMessages(newLocale)
38 | setLocale(newLocale)
39 | setMessages(messages)
40 | localStorage.setItem("locale", newLocale)
41 | }
42 |
43 | if (messages === null) {
44 | return null
45 | }
46 |
47 | return (
48 |
49 |
50 | {props.children}
51 |
52 |
53 | )
54 | }
55 |
56 | export const useLocale = (): LocaleContextProps => {
57 | const context = useContext(LocaleContext)
58 | if (!context) {
59 | throw new Error("useLocale must be used within an I18nProvider")
60 | }
61 | return context
62 | }
63 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/hooks/use-module-hotkeys.ts:
--------------------------------------------------------------------------------
1 | import type { Module } from "@/lib/modules"
2 | import modules from "@/modules"
3 | import { useMemo } from "react"
4 | import type { ComponentType } from "react"
5 | import { useIntl } from "react-intl"
6 |
7 | type ModuleHotkey = {
8 | method: string
9 | keystroke: string
10 | description: string
11 | }
12 |
13 | type ModuleMethod> = keyof NonNullable<
14 | M["hotkeys"]
15 | >
16 |
17 | export function useModuleHotkeys>(module: M) {
18 | const intl = useIntl()
19 |
20 | const hotkeysMap = useMemo(() => {
21 | const hotkeys: Partial, ModuleHotkey>> = {}
22 |
23 | const mod = modules.find((m) => m.id === module.id)
24 | if (!mod || !mod.hotkeys) return hotkeys
25 |
26 | for (const [method, key] of Object.entries(mod.hotkeys)) {
27 | hotkeys[method as ModuleMethod] = {
28 | method,
29 | keystroke: key as string,
30 | description: intl.formatMessage({ id: `${mod.id}.hotkeys.${method}` })
31 | }
32 | }
33 |
34 | return hotkeys
35 | }, [intl, module.id])
36 |
37 | const getHotkey = (method: ModuleMethod) => hotkeysMap[method]
38 |
39 | return { hotkeys: hotkeysMap, getHotkey }
40 | }
41 |
42 | export function useAllModuleHotkeys() {
43 | const intl = useIntl()
44 |
45 | return useMemo(() => {
46 | const hotkeys: Record = {}
47 |
48 | for (const mod of modules) {
49 | if (!mod.hotkeys) continue
50 |
51 | const moduleTitle = intl.formatMessage({ id: `${mod.id}.title` })
52 |
53 | hotkeys[moduleTitle] = Object.entries(mod.hotkeys).map(
54 | ([method, key]) => ({
55 | method,
56 | keystroke: key as string,
57 | description: intl.formatMessage({ id: `${mod.id}.hotkeys.${method}` })
58 | })
59 | )
60 | }
61 |
62 | return hotkeys
63 | }, [intl])
64 | }
65 |
--------------------------------------------------------------------------------
/packages/pinorama-transport/tests/unit.test.mts:
--------------------------------------------------------------------------------
1 | import { PinoramaClient } from "pinorama-client"
2 | import { afterEach, describe, expect, it, vi } from "vitest"
3 | import pinoramaTransport, { filterOptions } from "../src/lib.mts"
4 |
5 | const exampleOptions = {
6 | url: "http://example.com",
7 | maxRetries: 3,
8 | backoff: 500,
9 | backoffFactor: 2,
10 | backoffMax: 10000,
11 | adminSecret: "secret123",
12 | batchSize: 50, // not a client option
13 | flushInterval: 2000 // not a client option
14 | }
15 |
16 | vi.mock("pinorama-client", async (importOriginal) => {
17 | const mod = await importOriginal()
18 | return {
19 | ...mod,
20 | PinoramaClient: vi.fn()
21 | }
22 | })
23 |
24 | describe("pinoramaTransport", () => {
25 | const mockedClient = vi.mocked(PinoramaClient)
26 |
27 | afterEach(() => {
28 | vi.restoreAllMocks()
29 | })
30 |
31 | it("should initialize client with correct options", async () => {
32 | pinoramaTransport(exampleOptions)
33 |
34 | expect(mockedClient).toHaveBeenCalledWith({
35 | url: "http://example.com",
36 | maxRetries: 3,
37 | backoff: 500,
38 | backoffFactor: 2,
39 | backoffMax: 10000,
40 | adminSecret: "secret123"
41 | })
42 | })
43 | })
44 |
45 | describe("filterOptions", () => {
46 | it("should return an object with only specified keys", async () => {
47 | const filtered = filterOptions(exampleOptions, [
48 | "url",
49 | "maxRetries",
50 | "backoff"
51 | ])
52 |
53 | expect(filtered).toEqual({
54 | url: exampleOptions.url,
55 | maxRetries: exampleOptions.maxRetries,
56 | backoff: exampleOptions.backoff
57 | })
58 | })
59 |
60 | it("should not modify the original options object", async () => {
61 | const originalOptions = { ...exampleOptions }
62 | filterOptions(exampleOptions, ["url"])
63 |
64 | expect(exampleOptions).toEqual(originalOptions)
65 | })
66 | })
67 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/contexts/app-config-context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useState } from "react"
2 |
3 | export type AppConfig = {
4 | connectionIntent: boolean
5 | connectionUrl?: string | null
6 | liveMode: boolean
7 | }
8 |
9 | type AppConfigContextType = {
10 | config: AppConfig
11 | setConfig: (config: AppConfig) => void
12 | }
13 |
14 | const DEFAULT_CONFIG: AppConfig = {
15 | connectionIntent: false,
16 | connectionUrl: "http://localhost:6200",
17 | liveMode: false
18 | }
19 |
20 | const getAppConfigFromQueryParams = () => {
21 | const appConfig: Partial = {}
22 | const params = new URLSearchParams(window.location.search)
23 |
24 | const connectionUrl = params.get("connectionUrl")
25 | if (connectionUrl) {
26 | appConfig.connectionUrl = connectionUrl
27 | }
28 |
29 | const liveMode = params.get("liveMode")
30 | if (liveMode) {
31 | appConfig.liveMode = liveMode === "true"
32 | }
33 |
34 | return appConfig
35 | }
36 |
37 | const AppConfigContext = createContext(null)
38 |
39 | export function AppConfigProvider(props: { children: React.ReactNode }) {
40 | const [config, setConfig] = useState(DEFAULT_CONFIG)
41 |
42 | useEffect(() => {
43 | const queryConfig = getAppConfigFromQueryParams()
44 |
45 | const autoConnect = !!queryConfig.connectionUrl
46 | if (autoConnect) {
47 | queryConfig.connectionIntent = true
48 | }
49 |
50 | setConfig((prevConfig) => ({ ...prevConfig, ...queryConfig }))
51 | }, [])
52 |
53 | return (
54 |
55 | {props.children}
56 |
57 | )
58 | }
59 |
60 | export const useAppConfig = () => {
61 | const context = useContext(AppConfigContext)
62 |
63 | if (context === undefined) {
64 | throw new Error("useAppConfig must be used within a AppConfigProvider")
65 | }
66 |
67 | return context
68 | }
69 |
--------------------------------------------------------------------------------
/packages/pinorama-client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pinorama-client",
3 | "version": "0.2.1",
4 | "license": "MIT",
5 | "main": "./dist/node/pinorama-client.cjs",
6 | "module": "./dist/node/pinorama-client.mjs",
7 | "exports": {
8 | ".": {
9 | "types": "./dist/types/pinorama-client.d.ts",
10 | "require": "./dist/node/pinorama-client.cjs",
11 | "import": "./dist/node/pinorama-client.mjs"
12 | },
13 | "./node": {
14 | "types": "./dist/types/pinorama-client.d.ts",
15 | "require": "./dist/node/pinorama-client.cjs",
16 | "import": "./dist/node/pinorama-client.mjs"
17 | },
18 | "./browser": {
19 | "types": "./dist/types/pinorama-client.d.ts",
20 | "import": "./dist/browser/pinorama-client.esm.js"
21 | }
22 | },
23 | "types": "./dist/types/pinorama-client.d.ts",
24 | "files": ["dist"],
25 | "scripts": {
26 | "clean": "rimraf dist node_modules",
27 | "build": "rollup --config"
28 | },
29 | "homepage": "https://github.com/pinoramajs/pinorama#readme",
30 | "bugs": {
31 | "url": "https://github.com/pinoramajs/pinorama/issues"
32 | },
33 | "author": "Francesco Pasqua (https://cesco.me)",
34 | "repository": {
35 | "type": "git",
36 | "url": "https://github.com/pinoramajs/pinorama.git",
37 | "directory": "packages/pinorama-client"
38 | },
39 | "devDependencies": {
40 | "@orama/orama": "^3.0.4",
41 | "@rollup/plugin-alias": "^5.1.0",
42 | "@rollup/plugin-commonjs": "^26.0.1",
43 | "@rollup/plugin-node-resolve": "^15.2.3",
44 | "@rollup/plugin-terser": "^0.4.4",
45 | "@rollup/plugin-typescript": "^11.1.6",
46 | "@types/node": "^20.14.2",
47 | "pinorama-types": "workspace:*",
48 | "rimraf": "^5.0.7",
49 | "rollup": "^4.18.0",
50 | "rollup-plugin-dts": "^6.1.1",
51 | "tslib": "^2.6.3",
52 | "typescript": "^5.4.5"
53 | },
54 | "dependencies": {
55 | "zod": "^3.23.8"
56 | },
57 | "publishConfig": {
58 | "access": "public"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # pinorama-studio
2 |
3 | ## 0.4.0
4 |
5 | ### Minor Changes
6 |
7 | - f7dc713: keyboard shortcut system and ui enhancements
8 |
9 | ### Patch Changes
10 |
11 | - 06e7ae4: fix cli help message for batchSize and flushInterval
12 |
13 | ## 0.3.0
14 |
15 | ### Minor Changes
16 |
17 | - 4980b4b: add preset feature
18 | - a0495f4: add column visibility and sizing
19 |
20 | ### Patch Changes
21 |
22 | - 81c2135: set high frequency log flushing defaults
23 | - Updated dependencies [4980b4b]
24 | - Updated dependencies [a0495f4]
25 | - pinorama-presets@0.1.0
26 | - pinorama-transport@0.1.3
27 | - pinorama-client@0.2.1
28 | - pinorama-server@0.2.1
29 |
30 | ## 0.2.2
31 |
32 | ### Patch Changes
33 |
34 | - b33e94d: add refresh data button
35 |
36 | ## 0.2.1
37 |
38 | ### Patch Changes
39 |
40 | - 2ff89b8: wrong query param for connectionUrl
41 |
42 | ## 0.2.0
43 |
44 | ### Minor Changes
45 |
46 | - 306a952: add live mode feature
47 |
48 | ### Patch Changes
49 |
50 | - 54639cb: update favicon to official logo
51 | - 0b8ae5b: chore: optimize code and improve type safety
52 | - Updated dependencies [0b8ae5b]
53 | - Updated dependencies [306a952]
54 | - Updated dependencies [f79e807]
55 | - pinorama-client@0.2.0
56 | - pinorama-server@0.2.0
57 | - pinorama-transport@0.1.2
58 |
59 | ## 0.1.4
60 |
61 | ### Patch Changes
62 |
63 | - Updated dependencies [b31f88c]
64 | - pinorama-client@0.1.3
65 | - pinorama-server@0.1.2
66 | - pinorama-transport@0.1.2
67 |
68 | ## 0.1.3
69 |
70 | ### Patch Changes
71 |
72 | - 5a53ded: execute build step on github release workflow
73 |
74 | ## 0.1.2
75 |
76 | ### Patch Changes
77 |
78 | - ed89795: Add repository, bugs, homepage, and author info in `package.json` file
79 | - Updated dependencies [ed89795]
80 | - pinorama-transport@0.1.1
81 | - pinorama-client@0.1.2
82 | - pinorama-server@0.1.1
83 |
84 | ## 0.1.1
85 |
86 | ### Patch Changes
87 |
88 | - Updated dependencies [1059f37]
89 | - pinorama-client@0.1.1
90 | - pinorama-transport@0.1.0
91 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-filters/components/facet-body.tsx:
--------------------------------------------------------------------------------
1 | import { FacetItem } from "./facet-item"
2 |
3 | import type { AnySchema } from "@orama/orama"
4 | import { useVirtualizer } from "@tanstack/react-virtual"
5 | import type { IntrospectionFacet, PinoramaIntrospection } from "pinorama-types"
6 | import { useRef } from "react"
7 | import type { FacetValue, SearchFilters } from "../types"
8 |
9 | type FacetBodyProps = {
10 | introspection: PinoramaIntrospection
11 | name: string
12 | type: IntrospectionFacet
13 | values: FacetValue[]
14 | filters: SearchFilters
15 | onFiltersChange: (filters: SearchFilters) => void
16 | }
17 |
18 | export function FacetBody(props: FacetBodyProps) {
19 | const parentRef = useRef(null)
20 |
21 | const rowVirtualizer = useVirtualizer({
22 | count: props.values.length,
23 | getScrollElement: () => parentRef.current,
24 | estimateSize: () => 38
25 | })
26 |
27 | return (
28 |
32 |
36 | {rowVirtualizer.getVirtualItems().map((virtualItem) => (
37 |
45 |
54 |
55 | ))}
56 |
57 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/contexts/theme-context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useState } from "react"
2 |
3 | type Theme = "dark" | "light" | "system"
4 |
5 | type ThemeProviderProps = {
6 | children: React.ReactNode
7 | defaultTheme?: Theme
8 | storageKey?: string
9 | }
10 |
11 | type ThemeProviderState = {
12 | theme: Theme
13 | setTheme: (theme: Theme) => void
14 | }
15 |
16 | export const Theme = Object.freeze({
17 | Dark: "dark",
18 | Light: "light",
19 | System: "system"
20 | }) satisfies Readonly, Theme>>
21 |
22 | const initialState: ThemeProviderState = {
23 | theme: Theme.System,
24 | setTheme: () => null
25 | }
26 |
27 | const ThemeProviderContext = createContext(initialState)
28 |
29 | export function ThemeProvider({
30 | children,
31 | defaultTheme = Theme.Dark,
32 | storageKey = "pinorama-theme",
33 | ...props
34 | }: ThemeProviderProps) {
35 | const [theme, setTheme] = useState(
36 | () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
37 | )
38 |
39 | useEffect(() => {
40 | const root = window.document.documentElement
41 |
42 | root.classList.remove(Theme.Light, Theme.Dark)
43 |
44 | if (theme === Theme.System) {
45 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
46 | .matches
47 | ? Theme.Dark
48 | : Theme.Light
49 |
50 | root.classList.add(systemTheme)
51 | return
52 | }
53 |
54 | root.classList.add(theme)
55 | }, [theme])
56 |
57 | const value = {
58 | theme,
59 | setTheme: (theme: Theme) => {
60 | localStorage.setItem(storageKey, theme)
61 | setTheme(theme)
62 | }
63 | }
64 |
65 | return (
66 |
67 | {children}
68 |
69 | )
70 | }
71 |
72 | export const useTheme = () => {
73 | const context = useContext(ThemeProviderContext)
74 |
75 | if (context === undefined)
76 | throw new Error("useTheme must be used within a ThemeProvider")
77 |
78 | return context
79 | }
80 |
--------------------------------------------------------------------------------
/packages/pinorama-docs/guide/quick-start.md:
--------------------------------------------------------------------------------
1 | ---
2 | outline: deep
3 | ---
4 |
5 | # Quick Start
6 |
7 | Get up and running with **Pinorama Studio** in minutes! This guide will walk you through the process of installing Pinorama Studio and integrating it with a simple Node.js application.
8 |
9 | ## Prerequisites
10 |
11 | - [Node.js](https://nodejs.org/) version 20 or higher.
12 | - Terminal for starting the Pinorama Studio CLI.
13 |
14 | ## Installation
15 |
16 | Install Pinorama Studio globally:
17 |
18 | ::: code-group
19 |
20 | ```sh [npm]
21 | npm i -g pinorama-studio
22 | ```
23 |
24 | ```sh [pnpm]
25 | pnpm i -g pinorama-studio
26 | ```
27 |
28 | ```sh [yarn]
29 | yarn global add pinorama-studio
30 | ```
31 |
32 | :::
33 |
34 | ## Quick Setup
35 |
36 | Let's set up a minimal Fastify application with Pinorama Studio for log viewing:
37 |
38 | 1. Create a new directory:
39 |
40 | ```sh
41 | mkdir pinorama-demo && cd pinorama-demo
42 | ```
43 |
44 | 2. Install Fastify:
45 |
46 | ::: code-group
47 |
48 | ```sh [npm]
49 | npm add fastify
50 | ```
51 |
52 | ```sh [pnpm]
53 | pnpm add fastify
54 | ```
55 |
56 | ```sh [yarn]
57 | yarn add fastify
58 | ```
59 |
60 | :::
61 |
62 | 3. Create an `index.js` file with the following content:
63 |
64 | ```javascript
65 | const fastify = require("fastify")({
66 | logger: true, // needed for piping logs to Pinorama!
67 | });
68 |
69 | fastify.get("/", async (request, reply) => {
70 | request.log.info("Pinorama is awesome! 🚀");
71 | return { hello: "world" };
72 | });
73 |
74 | fastify.listen({ port: 3000 });
75 | ```
76 |
77 | 4. Run your application and pipe the output to Pinorama Studio:
78 |
79 | ```sh
80 | node index.js | pinorama --open
81 | ```
82 |
83 | This command will start your Fastify application, pipe its logs to Pinorama Studio, and open the Pinorama web interface in your default browser.
84 |
85 | ## Next Steps
86 |
87 | Check out the full Pinorama documentation for information on customizing your logging setup and using other Pinorama components.
88 |
89 | Happy logging with Pinorama!
90 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-viewer/components/header/toggle-live-button.tsx:
--------------------------------------------------------------------------------
1 | import { Kbd } from "@/components/kbd/kbd"
2 | import { Toggle } from "@/components/ui/toggle"
3 | import { TooltipContent } from "@/components/ui/tooltip"
4 | import { useModuleHotkeys } from "@/hooks/use-module-hotkeys"
5 | import { cn } from "@/lib/utils"
6 | import LogExplorerModule from "@/modules/log-explorer"
7 | import { Tooltip, TooltipPortal, TooltipTrigger } from "@radix-ui/react-tooltip"
8 | import { PlayCircleIcon, StopCircleIcon } from "lucide-react"
9 | import { useIntl } from "react-intl"
10 |
11 | type ToggleLiveButtonProps = {
12 | pressed?: boolean
13 | onPressedChange?: (pressed: boolean) => void
14 | }
15 |
16 | export function ToggleLiveButton(props: ToggleLiveButtonProps) {
17 | const intl = useIntl()
18 | const moduleHotkeys = useModuleHotkeys(LogExplorerModule)
19 |
20 | const label = intl.formatMessage({ id: "logExplorer.liveMode" })
21 | const hotkey = moduleHotkeys.getHotkey("liveMode")
22 | const Icon = props.pressed ? StopCircleIcon : PlayCircleIcon
23 |
24 | return (
25 |
26 |
27 |
37 |
46 |
47 |
48 |
49 |
50 | {hotkey?.description}
51 | {hotkey?.keystroke}
52 |
53 |
54 |
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "@radix-ui/react-slot"
2 | import { type VariantProps, cva } from "class-variance-authority"
3 | import * as React from "react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | outline2:
18 | "border border-input bg-muted/20 hover:bg-accent hover:text-accent-foreground",
19 | secondary:
20 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
21 | ghost: "hover:bg-accent hover:text-accent-foreground",
22 | link: "text-primary underline-offset-4 hover:underline"
23 | },
24 | size: {
25 | default: "h-10 px-4 py-2",
26 | badge: "rounded-full px-1 py-0.5",
27 | sm: "h-8 rounded-md px-3",
28 | lg: "h-11 rounded-md px-8",
29 | icon: "h-10 w-10",
30 | xs: "h-6 px-2"
31 | }
32 | },
33 | defaultVariants: {
34 | variant: "default",
35 | size: "default"
36 | }
37 | }
38 | )
39 |
40 | export interface ButtonProps
41 | extends React.ButtonHTMLAttributes,
42 | VariantProps {
43 | asChild?: boolean
44 | }
45 |
46 | const Button = React.forwardRef(
47 | ({ className, variant, size, asChild = false, ...props }, ref) => {
48 | const Comp = asChild ? Slot : "button"
49 | return (
50 |
55 | )
56 | }
57 | )
58 | Button.displayName = "Button"
59 |
60 | export { Button, buttonVariants }
61 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-viewer/components/header/toggle-columns-button.tsx:
--------------------------------------------------------------------------------
1 | import { IconButton } from "@/components/icon-button/icon-button"
2 | import {
3 | DropdownMenu,
4 | DropdownMenuCheckboxItem,
5 | DropdownMenuContent,
6 | DropdownMenuItem,
7 | DropdownMenuSeparator,
8 | DropdownMenuTrigger
9 | } from "@/components/ui/dropdown-menu"
10 | import { createField } from "@/lib/introspection"
11 | import type { AnySchema } from "@orama/orama"
12 | import type { Table } from "@tanstack/react-table"
13 | import { ListChecksIcon } from "lucide-react"
14 | import type { PinoramaIntrospection } from "pinorama-types"
15 | import { FormattedMessage } from "react-intl"
16 |
17 | type ToggleColumnsButtonProps = {
18 | introspection: PinoramaIntrospection
19 | table: Table
20 | }
21 |
22 | export function ToggleColumnsButton(props: ToggleColumnsButtonProps) {
23 | return (
24 |
25 |
26 |
27 |
28 |
29 | {props.table
30 | .getAllColumns()
31 | .filter(
32 | (column) =>
33 | typeof column.accessorFn !== "undefined" && column.getCanHide()
34 | )
35 | .map((column) => {
36 | const field = createField(column.id, props.introspection)
37 | return (
38 | column.toggleVisibility(!!value)}
46 | >
47 | {field.getDisplayLabel()}
48 |
49 | )
50 | })}
51 |
52 | props.table.resetColumnVisibility()}>
53 |
54 |
55 |
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-viewer/hooks/use-live-logs.ts:
--------------------------------------------------------------------------------
1 | import { usePinoramaClient } from "@/contexts"
2 | import { buildPayload } from "@/modules/log-explorer/utils"
3 | import { useInfiniteQuery } from "@tanstack/react-query"
4 | import { useCallback, useEffect, useMemo, useRef } from "react"
5 |
6 | import type { AnyOrama, SearchParams } from "@orama/orama"
7 |
8 | const POLL_DELAY = 1500
9 |
10 | export const useLiveLogs = (
11 | searchText?: string,
12 | searchFilters?: SearchParams["where"],
13 | enabled?: boolean
14 | ) => {
15 | const client = usePinoramaClient()
16 | const timeoutRef = useRef(null)
17 |
18 | const query = useInfiniteQuery({
19 | queryKey: ["live-logs", searchText, searchFilters],
20 | queryFn: async ({ pageParam }) => {
21 | const payload = buildPayload(searchText, searchFilters, pageParam)
22 |
23 | const response = await client?.search(payload)
24 | const newData = response?.hits.map((hit) => hit.document) ?? []
25 |
26 | if (newData.length > 0) {
27 | const lastItem = newData[newData.length - 1]
28 | const metadata = lastItem._pinorama
29 | pageParam = metadata.createdAt
30 | }
31 |
32 | return { data: newData, nextCursor: pageParam }
33 | },
34 | initialPageParam: 0,
35 | getNextPageParam: (lastPage) => lastPage.nextCursor,
36 | staleTime: Number.POSITIVE_INFINITY,
37 | // refetchInterval: POLL_DELAY, // NOTE: This is not working as expected, it doesn't use the nextCursor
38 | enabled: false
39 | })
40 |
41 | const schedulePoll = useCallback(() => {
42 | if (enabled) {
43 | timeoutRef.current = setTimeout(() => {
44 | query.fetchNextPage().finally(schedulePoll)
45 | }, POLL_DELAY)
46 | }
47 | }, [enabled, query.fetchNextPage])
48 |
49 | useEffect(() => {
50 | if (enabled) {
51 | query.fetchNextPage().finally(() => {
52 | schedulePoll()
53 | })
54 | }
55 | return () => {
56 | if (timeoutRef.current) {
57 | clearTimeout(timeoutRef.current)
58 | }
59 | }
60 | }, [enabled, query.fetchNextPage, schedulePoll])
61 |
62 | const flattenedData = useMemo(() => {
63 | return query.data?.pages.flatMap((page) => page.data) ?? []
64 | }, [query.data])
65 |
66 | return { ...query, data: flattenedData }
67 | }
68 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/lib/modules.tsx:
--------------------------------------------------------------------------------
1 | import type { ImportMessages } from "@/i18n"
2 | import {
3 | type ComponentRef,
4 | type ComponentType,
5 | useEffect,
6 | useRef,
7 | useState
8 | } from "react"
9 | import { useHotkeys } from "react-hotkeys-hook"
10 |
11 | export type MethodKeys = {
12 | [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never
13 | }[keyof T]
14 |
15 | export type Hotkeys = Record, string>
16 |
17 | export type Module = {
18 | id: string
19 | routePath: string
20 | component: T
21 | messages?: ImportMessages
22 | hotkeys?: Hotkeys>
23 | }
24 |
25 | export function createModule(
26 | mod: Module
27 | ): Module {
28 | return mod.hotkeys
29 | ? { ...mod, component: withHotkeys(mod.component, mod.hotkeys) }
30 | : mod
31 | }
32 |
33 | function withHotkeys(
34 | WrappedComponent: T,
35 | hotkeys: Hotkeys>
36 | ): T {
37 | function ComponentWithHotkeys(props: any) {
38 | const ref = useRef>(null)
39 | const [hotkeyEnabled, setHotkeyEnabled] = useState(true)
40 |
41 | useEffect(() => {
42 | const attrName = "data-scroll-locked"
43 |
44 | const checkDataAttribute = () => {
45 | const bodyDataAttribute = document.body.getAttribute(attrName)
46 | setHotkeyEnabled(bodyDataAttribute !== "1")
47 | }
48 |
49 | checkDataAttribute()
50 |
51 | const observer = new MutationObserver(checkDataAttribute)
52 | observer.observe(document.body, {
53 | attributes: true,
54 | attributeFilter: [attrName]
55 | })
56 |
57 | return () => observer.disconnect()
58 | }, [])
59 |
60 | for (const [method, key] of Object.entries(hotkeys)) {
61 | useHotkeys(
62 | key as string,
63 | (event) => {
64 | event.preventDefault()
65 | if (ref.current) {
66 | const func = ref.current[method as keyof ComponentRef]
67 | if (typeof func === "function") {
68 | func.call(ref.current)
69 | } else {
70 | console.warn(`Method '${method}' not found or not a function`)
71 | }
72 | }
73 | },
74 | {
75 | enabled: hotkeyEnabled
76 | }
77 | )
78 | }
79 |
80 | return
81 | }
82 |
83 | return ComponentWithHotkeys as T
84 | }
85 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/hooks/use-pinorama-connection.ts:
--------------------------------------------------------------------------------
1 | import { useAppConfig } from "@/contexts"
2 | import { useEffect, useState } from "react"
3 | import { usePinoramaIntrospection } from "./use-pinorama-introspection"
4 | import { usePinoramaStyles } from "./use-pinorama-styles"
5 |
6 | export type ConnectionStatus =
7 | | "disconnected"
8 | | "connecting"
9 | | "connected"
10 | | "failed"
11 | | "unknown"
12 |
13 | export const ConnectionStatus = Object.freeze({
14 | Disconnected: "disconnected",
15 | Connecting: "connecting",
16 | Connected: "connected",
17 | Failed: "failed",
18 | Unknown: "unknown"
19 | }) satisfies Readonly, ConnectionStatus>>
20 |
21 | type ConnectionStatusDetail = {
22 | connectionStatus: ConnectionStatus
23 | isConnected?: boolean
24 | }
25 |
26 | export function usePinoramaConnection() {
27 | const appConfig = useAppConfig()
28 |
29 | const introspection = usePinoramaIntrospection()
30 | usePinoramaStyles()
31 |
32 | const [{ connectionStatus, isConnected = false }, setConnectionStatus] =
33 | useState({
34 | connectionStatus: ConnectionStatus.Unknown
35 | })
36 |
37 | useEffect(() => {
38 | switch (true) {
39 | case appConfig?.config.connectionIntent === false:
40 | setConnectionStatus({ connectionStatus: ConnectionStatus.Disconnected })
41 | break
42 | case introspection.status === "pending" &&
43 | introspection.fetchStatus === "fetching":
44 | setConnectionStatus({ connectionStatus: ConnectionStatus.Connecting })
45 | break
46 | case introspection.status === "success":
47 | setConnectionStatus({
48 | connectionStatus: ConnectionStatus.Connected,
49 | isConnected: true
50 | })
51 | break
52 | case introspection.status === "error":
53 | setConnectionStatus({ connectionStatus: ConnectionStatus.Failed })
54 | break
55 | default:
56 | setConnectionStatus({ connectionStatus: ConnectionStatus.Unknown })
57 | break
58 | }
59 | }, [
60 | appConfig?.config.connectionIntent,
61 | introspection.status,
62 | introspection.fetchStatus
63 | ])
64 |
65 | const toggleConnection = () => {
66 | appConfig?.setConfig({
67 | ...appConfig.config,
68 | connectionIntent: !appConfig.config.connectionIntent
69 | })
70 | }
71 |
72 | return {
73 | connectionStatus,
74 | toggleConnection,
75 | isConnected,
76 | introspection: introspection.data
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/title-bar/components/hotkeys-button.tsx:
--------------------------------------------------------------------------------
1 | import { Kbd } from "@/components/kbd/kbd"
2 | import { Button } from "@/components/ui/button"
3 | import {
4 | Dialog,
5 | DialogContent,
6 | DialogDescription,
7 | DialogHeader,
8 | DialogTitle,
9 | DialogTrigger
10 | } from "@/components/ui/dialog"
11 | import {
12 | Tooltip,
13 | TooltipContent,
14 | TooltipPortal,
15 | TooltipTrigger
16 | } from "@/components/ui/tooltip"
17 | import { useAllModuleHotkeys } from "@/hooks/use-module-hotkeys"
18 | import { KeyboardIcon } from "lucide-react"
19 | import { FormattedMessage } from "react-intl"
20 |
21 | export function HotkeysButton() {
22 | const hotkeys = useAllModuleHotkeys()
23 |
24 | const handleClick = () => {}
25 |
26 | return (
27 |
76 | )
77 | }
78 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | "./pages/**/*.{ts,tsx}",
6 | "./components/**/*.{ts,tsx}",
7 | "./app/**/*.{ts,tsx}",
8 | "./src/**/*.{ts,tsx}"
9 | ],
10 | prefix: "",
11 | theme: {
12 | container: {
13 | center: true,
14 | padding: "2rem",
15 | screens: {
16 | "2xl": "1400px"
17 | }
18 | },
19 | extend: {
20 | fontFamily: {
21 | mono: ["IBM Plex Mono"],
22 | sans: ["Roboto"]
23 | },
24 | colors: {
25 | border: "hsl(var(--border))",
26 | input: "hsl(var(--input))",
27 | ring: "hsl(var(--ring))",
28 | background: "hsl(var(--background))",
29 | foreground: "hsl(var(--foreground))",
30 | primary: {
31 | DEFAULT: "hsl(var(--primary))",
32 | foreground: "hsl(var(--primary-foreground))"
33 | },
34 | secondary: {
35 | DEFAULT: "hsl(var(--secondary))",
36 | foreground: "hsl(var(--secondary-foreground))"
37 | },
38 | destructive: {
39 | DEFAULT: "hsl(var(--destructive))",
40 | foreground: "hsl(var(--destructive-foreground))"
41 | },
42 | muted: {
43 | DEFAULT: "hsl(var(--muted))",
44 | foreground: "hsl(var(--muted-foreground))"
45 | },
46 | accent: {
47 | DEFAULT: "hsl(var(--accent))",
48 | foreground: "hsl(var(--accent-foreground))"
49 | },
50 | popover: {
51 | DEFAULT: "hsl(var(--popover))",
52 | foreground: "hsl(var(--popover-foreground))"
53 | },
54 | card: {
55 | DEFAULT: "hsl(var(--card))",
56 | foreground: "hsl(var(--card-foreground))"
57 | }
58 | },
59 | borderRadius: {
60 | lg: "var(--radius)",
61 | md: "calc(var(--radius) - 2px)",
62 | sm: "calc(var(--radius) - 4px)"
63 | },
64 | keyframes: {
65 | "accordion-down": {
66 | from: { height: "0" },
67 | to: { height: "var(--radix-accordion-content-height)" }
68 | },
69 | "accordion-up": {
70 | from: { height: "var(--radix-accordion-content-height)" },
71 | to: { height: "0" }
72 | }
73 | },
74 | animation: {
75 | "accordion-down": "accordion-down 0.2s ease-out",
76 | "accordion-up": "accordion-up 0.2s ease-out"
77 | }
78 | }
79 | },
80 | plugins: [
81 | require("tailwindcss-animate"),
82 | require("@tailwind-plugin/expose-colors")({ prefix: "--color" })
83 | ]
84 | }
85 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-viewer/utils/index.tsx:
--------------------------------------------------------------------------------
1 | import { createField } from "@/lib/introspection"
2 | import { cn } from "@/lib/utils"
3 | import type { AnySchema } from "@orama/orama"
4 | import type { ColumnDef, Table } from "@tanstack/react-table"
5 | import debounce from "debounce"
6 | import type { PinoramaIntrospection } from "pinorama-types"
7 |
8 | const DEFAULT_COLUMN_SIZE = 150
9 |
10 | export const getColumnsConfig = (
11 | introspection: PinoramaIntrospection
12 | ) => {
13 | const visibility: Record = {}
14 | const sizing: Record = {}
15 | const definition: ColumnDef[] = []
16 |
17 | const columns = introspection?.columns
18 | if (!columns)
19 | return {
20 | visibility,
21 | sizing,
22 | definition
23 | }
24 |
25 | for (const [name, config] of Object.entries(columns)) {
26 | // Visibility
27 | visibility[name] = config?.visible ?? false
28 |
29 | // Sizing
30 | sizing[name] = config?.size ?? DEFAULT_COLUMN_SIZE
31 |
32 | // Definition
33 | const field = createField(name, introspection)
34 | definition.push({
35 | id: name,
36 | // accessorKey: columnName.split(".")[0] || columnName,
37 | accessorKey: name,
38 | header: () => field.getDisplayLabel(),
39 | cell: (info) => {
40 | const value = info.getValue() as string | number
41 | if (!value) {
42 | return —
43 | }
44 |
45 | const formattedValue = field.format(value)
46 | const className = field.getClassName(value)
47 | return (
48 |
49 | {formattedValue}
50 |
51 | )
52 | }
53 | })
54 | }
55 |
56 | return {
57 | visibility,
58 | sizing,
59 | definition
60 | }
61 | }
62 |
63 | const debouncedScrollIntoView = debounce((element: Element) => {
64 | element.scrollIntoView({
65 | block: "center",
66 | behavior: "smooth"
67 | })
68 | }, 50)
69 |
70 | export const selectRowByIndex = (index: number, table: Table) => {
71 | const totalRows = table.getRowModel().rows.length
72 | const validIndex = Math.max(0, Math.min(index, totalRows - 1))
73 |
74 | table.setRowSelection({ [index]: true })
75 |
76 | const row = document.querySelector(`[data-index="${validIndex}"]`)
77 | if (row) {
78 | debouncedScrollIntoView(row)
79 | }
80 | }
81 |
82 | export const getCurrentRowIndex = (table: Table) => {
83 | const selectedKeys = table.getSelectedRowModel()
84 | return selectedKeys.rows[0]?.index ?? -1
85 | }
86 |
87 | export const canSelectNextRow = (table: Table) => {
88 | const currentRowIndex = getCurrentRowIndex(table)
89 | return currentRowIndex < table.getRowModel().rows.length - 1
90 | }
91 |
92 | export const canSelectPreviousRow = (table: Table) => {
93 | const currentRowIndex = getCurrentRowIndex(table)
94 | return currentRowIndex > 0
95 | }
96 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pinorama-studio",
3 | "version": "0.4.0",
4 | "license": "MIT",
5 | "type": "module",
6 | "bin": {
7 | "pinorama": "./cli.mjs"
8 | },
9 | "files": ["dist", "cli.mjs"],
10 | "scripts": {
11 | "dev": "vite",
12 | "build": "tsc && vite build",
13 | "preview": "vite preview",
14 | "clean": "rimraf dist node_modules"
15 | },
16 | "homepage": "https://github.com/pinoramajs/pinorama#readme",
17 | "bugs": {
18 | "url": "https://github.com/pinoramajs/pinorama/issues"
19 | },
20 | "author": "Francesco Pasqua (https://cesco.me)",
21 | "repository": {
22 | "type": "git",
23 | "url": "https://github.com/pinoramajs/pinorama.git",
24 | "directory": "packages/pinorama-studio"
25 | },
26 | "dependencies": {
27 | "@fastify/cors": "^10.0.2",
28 | "@fastify/one-line-logger": "^2.0.2",
29 | "@fastify/static": "^8.0.4",
30 | "@hookform/resolvers": "^3.9.0",
31 | "@radix-ui/react-checkbox": "^1.1.1",
32 | "@radix-ui/react-dialog": "^1.1.1",
33 | "@radix-ui/react-dropdown-menu": "^2.1.1",
34 | "@radix-ui/react-label": "^2.1.0",
35 | "@radix-ui/react-popover": "^1.1.1",
36 | "@radix-ui/react-select": "^2.1.1",
37 | "@radix-ui/react-separator": "^1.1.0",
38 | "@radix-ui/react-slot": "^1.1.0",
39 | "@radix-ui/react-toast": "^1.2.1",
40 | "@radix-ui/react-toggle": "^1.1.0",
41 | "@radix-ui/react-tooltip": "^1.1.2",
42 | "@tanstack/react-query": "^5.50.1",
43 | "@tanstack/react-router": "^1.45.4",
44 | "@tanstack/react-table": "^8.19.2",
45 | "@tanstack/react-virtual": "^3.8.2",
46 | "chalk": "^5.3.0",
47 | "change-case": "^5.4.4",
48 | "class-variance-authority": "^0.7.0",
49 | "clsx": "^2.1.1",
50 | "date-fns": "^3.6.0",
51 | "debounce": "^2.1.0",
52 | "fastify": "^5.2.1",
53 | "lucide-react": "^0.390.0",
54 | "minimist": "^1.2.8",
55 | "open": "^10.1.0",
56 | "pinorama-client": "workspace:*",
57 | "pinorama-presets": "workspace:*",
58 | "pinorama-server": "workspace:*",
59 | "pinorama-transport": "workspace:*",
60 | "react": "^18.3.1",
61 | "react-dom": "^18.3.1",
62 | "react-hook-form": "^7.52.1",
63 | "react-hotkeys-hook": "^4.5.0",
64 | "react-intl": "^6.6.8",
65 | "react-json-view-lite": "^1.4.0",
66 | "react-resizable-panels": "^2.0.20",
67 | "tailwind-merge": "^2.4.0",
68 | "tailwindcss-animate": "^1.0.7",
69 | "zod": "^3.23.8"
70 | },
71 | "devDependencies": {
72 | "@orama/orama": "^3.0.4",
73 | "@tailwind-plugin/expose-colors": "^1.1.8",
74 | "@types/node": "^20.14.10",
75 | "@types/react": "^18.3.3",
76 | "@types/react-dom": "^18.3.0",
77 | "@vitejs/plugin-react": "^4.3.1",
78 | "autoprefixer": "^10.4.19",
79 | "pinorama-types": "workspace:*",
80 | "postcss": "^8.4.39",
81 | "rimraf": "^5.0.9",
82 | "tailwindcss": "^3.4.4",
83 | "typescript": "^5.5.3",
84 | "vite": "^5.3.3",
85 | "vite-plugin-package-version": "^1.1.0"
86 | },
87 | "publishConfig": {
88 | "access": "public"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/packages/pinorama-client/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import alias from "@rollup/plugin-alias"
2 | import commonjs from "@rollup/plugin-commonjs"
3 | import resolve from "@rollup/plugin-node-resolve"
4 | import terser from "@rollup/plugin-terser"
5 | import typescript from "@rollup/plugin-typescript"
6 | import { dts } from "rollup-plugin-dts"
7 |
8 | const inputFile = "src/index.ts"
9 | const outputFileName = "pinorama-client"
10 |
11 | export default [
12 | // Declaration file
13 | {
14 | input: inputFile,
15 | output: [{ file: "dist/types/pinorama-client.d.ts", format: "es" }],
16 | plugins: [dts()]
17 | },
18 |
19 | // Browser ESM build
20 | {
21 | input: inputFile,
22 | output: [
23 | {
24 | file: `dist/browser/${outputFileName}.esm.js`,
25 | format: "es",
26 | sourcemap: true
27 | }
28 | ],
29 | plugins: [
30 | alias({
31 | entries: [
32 | { find: "./platform/node.js", replacement: "./platform/browser.js" }
33 | ]
34 | }),
35 | resolve({ browser: true }),
36 | commonjs(),
37 | typescript(),
38 | terser()
39 | ],
40 | external: ["zod"]
41 | },
42 |
43 | // // Browser UMD build
44 | // {
45 | // input: inputFile,
46 | // output: [
47 | // {
48 | // file: `dist/browser/${outputFileName}.umd.js`,
49 | // format: "umd",
50 | // name: "pinorama-client",
51 | // },
52 | // ],
53 | // plugins: [
54 | // alias({
55 | // entries: [
56 | // { find: "./platform/node.js", replacement: "./platform/browser.js" },
57 | // ],
58 | // }),
59 | // resolve({ browser: true }),
60 | // commonjs(),
61 | // typescript(),
62 | // ],
63 | // external: ["zod"],
64 | // },
65 |
66 | // // Browser CJS build
67 | // {
68 | // input: inputFile,
69 | // output: [
70 | // {
71 | // file: `dist/browser/${outputFileName}.cjs.js`,
72 | // format: "cjs",
73 | // },
74 | // ],
75 | // plugins: [
76 | // alias({
77 | // entries: [
78 | // { find: "./platform/node.js", replacement: "./platform/browser.js" },
79 | // ],
80 | // }),
81 | // resolve({ browser: true }),
82 | // commonjs(),
83 | // typescript(),
84 | // ],
85 | // external: ["zod"],
86 | // },
87 |
88 | // Node.js ESM bundle
89 | {
90 | input: inputFile,
91 | output: [
92 | {
93 | file: `dist/node/${outputFileName}.mjs`,
94 | format: "es",
95 | sourcemap: true
96 | }
97 | ],
98 | plugins: [resolve(), commonjs(), typescript()],
99 | external: ["zod"]
100 | },
101 |
102 | // Node.js CJS build
103 | {
104 | input: inputFile,
105 | output: [
106 | {
107 | file: `dist/node/${outputFileName}.cjs`,
108 | format: "cjs",
109 | sourcemap: true
110 | }
111 | ],
112 | plugins: [resolve(), commonjs(), typescript()],
113 | external: ["zod"]
114 | }
115 | ]
116 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pinorama 🌀
2 |
3 | 
4 | [](https://snyk.io/test/github/pinoramajs/pinorama)
5 | [](#contributors-)
6 |
7 | ⚠️ **Warning: This project is currently under development and not ready for production use.**
8 |
9 | Pinorama is a suite of node.js packages that allow you to store and explore [pino](https://getpino.io) logs. It contains:
10 |
11 | - **server:** http server that stores logs using [orama](https://askorama.ai) and exposes rest api using [fastify](https://fastify.dev).
12 | - **client:** http client that streams and retrieves logs from the pinorama-server.
13 | - **transport:** pino logger transport that streams logs to the pinorama-server.
14 | - **docs:** project documentation available online at [pinorama.dev](https://pinorama.dev) powered by [vitepress](https://vitepress.dev).
15 | - **studio:** standalone app that allows you to explore the logs using [react](https://react.dev) and [shadcn/ui](https://ui.shadcn.com).
16 |
17 | ---
18 |
19 | This README will be updated as the project evolves.
20 |
21 | ---
22 |
23 | ## Contributors ✨
24 |
25 |
26 |
27 |
28 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | This project follows the [all-contributors](https://allcontributors.org/) specification. Contributions of any kind welcome!
43 |
--------------------------------------------------------------------------------
/packages/pinorama-server/src/index.mts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs"
2 | import { create } from "@orama/orama"
3 | import { restoreFromFile } from "@orama/plugin-data-persistence/server"
4 | import Fastify from "fastify"
5 | import fp from "fastify-plugin"
6 |
7 | import { pino as defaultPreset } from "pinorama-presets"
8 | import * as plugins from "./plugins/index.mjs"
9 | import * as routes from "./routes/index.mjs"
10 | import { withPinoramaMetadataSchema } from "./utils/metadata.mjs"
11 |
12 | import type { AnyOrama, AnySchema } from "@orama/orama"
13 | import type {
14 | FastifyInstance,
15 | FastifyPluginAsync,
16 | FastifyRegisterOptions,
17 | FastifyServerOptions,
18 | LogLevel,
19 | RegisterOptions
20 | } from "fastify"
21 | import type { PinoramaIntrospection } from "pinorama-types"
22 |
23 | declare module "fastify" {
24 | interface FastifyInstance {
25 | pinoramaDb: AnyOrama
26 | pinoramaOpts: PinoramaServerOptions
27 | }
28 | }
29 |
30 | type PersistenceFormat = "json" | "dpack" | "binary" // orama does not export this type
31 |
32 | type PinoramaServerOptions = {
33 | adminSecret?: string
34 | dbSchema?: AnySchema
35 | dbPath?: string
36 | dbFormat?: PersistenceFormat
37 | prefix?: string
38 | logLevel?: LogLevel
39 | introspection: PinoramaIntrospection
40 | }
41 |
42 | export const defaultOptions: PinoramaServerOptions = {
43 | adminSecret: process.env.PINORAMA_SERVER_ADMIN_SECRET,
44 | dbFormat: "json",
45 | dbSchema: defaultPreset.schema,
46 | introspection: defaultPreset.introspection
47 | }
48 |
49 | const fastifyPinoramaServer: FastifyPluginAsync = async (
50 | fastify,
51 | options
52 | ) => {
53 | const opts = { ...defaultOptions, ...options }
54 |
55 | const db = fs.existsSync(opts.dbPath as string)
56 | ? await restoreFromFile(opts.dbFormat, opts.dbPath)
57 | : create({ schema: withPinoramaMetadataSchema(opts.dbSchema) })
58 |
59 | fastify.decorate("pinoramaOpts", opts)
60 | fastify.decorate("pinoramaDb", db)
61 |
62 | const registerOpts: RegisterOptions = {}
63 |
64 | if (opts.prefix) {
65 | registerOpts.prefix = opts.prefix
66 | }
67 |
68 | if (opts.logLevel) {
69 | registerOpts.logLevel = opts.logLevel
70 | }
71 |
72 | fastify.register(routes.bulkRoute, registerOpts)
73 | fastify.register(routes.introspectionRoute, registerOpts)
74 | fastify.register(routes.persistRoute, registerOpts)
75 | fastify.register(routes.searchRoute, registerOpts)
76 | fastify.register(routes.stylesRoute, registerOpts)
77 |
78 | fastify.register(plugins.gracefulSaveHook)
79 | fastify.register(plugins.authHook)
80 | }
81 |
82 | function createServer(
83 | pinoramaOptions: FastifyRegisterOptions,
84 | fastifyOptions?: FastifyServerOptions
85 | ): FastifyInstance {
86 | const fastify = Fastify(fastifyOptions)
87 | fastify.register(fastifyPinoramaServer, pinoramaOptions)
88 | return fastify
89 | }
90 |
91 | const plugin = fp(fastifyPinoramaServer, {
92 | fastify: "5.x",
93 | name: "fastify-pinorama-server"
94 | })
95 |
96 | export default plugin
97 | export { fastifyPinoramaServer, createServer }
98 |
--------------------------------------------------------------------------------
/packages/pinorama-transport/tests/integration.test.mts:
--------------------------------------------------------------------------------
1 | import { beforeEach, describe, expect, it } from "vitest"
2 |
3 | import { setTimeout } from "node:timers/promises"
4 | import { pino } from "pino"
5 | import { PinoramaClient } from "pinorama-client"
6 | import { createServer } from "pinorama-server"
7 | import pinoramaTransport from "../src/lib.mjs"
8 |
9 | describe("pinoramaTransport", async () => {
10 | let pinoramaClientInstance: PinoramaClient
11 | let pinoramaServerUrl: string
12 |
13 | beforeEach(async () => {
14 | const pinoramaServerInstance = createServer()
15 |
16 | await pinoramaServerInstance.listen()
17 | await pinoramaServerInstance.ready()
18 |
19 | const address = pinoramaServerInstance.server.address()
20 | const port = typeof address === "string" ? address : address?.port
21 |
22 | pinoramaServerUrl = `http://localhost:${port}`
23 | pinoramaClientInstance = new PinoramaClient({ url: pinoramaServerUrl })
24 |
25 | return async () => {
26 | await pinoramaServerInstance.close()
27 | }
28 | })
29 |
30 | it("should store a log line to pinorama server", async () => {
31 | const transport = pinoramaTransport({ url: pinoramaServerUrl })
32 | const log = pino(transport)
33 |
34 | // act
35 | log.info("hello world")
36 |
37 | setImmediate(() => transport.end())
38 | await setTimeout(100)
39 |
40 | const response = await pinoramaClientInstance.search({})
41 | expect(response.hits.length).toBe(1)
42 | expect(response.hits[0].document.msg).toBe("hello world")
43 | })
44 |
45 | it("should store a deeply nested log line to pinorama server", async () => {
46 | const transport = pinoramaTransport({ url: pinoramaServerUrl })
47 | const log = pino(transport)
48 |
49 | // act
50 | log.info({
51 | deeply: {
52 | nested: {
53 | hello: "world"
54 | }
55 | }
56 | })
57 |
58 | setImmediate(() => transport.end())
59 | await setTimeout(100)
60 |
61 | const response = await pinoramaClientInstance.search({})
62 | expect(response.hits.length).toBe(1)
63 | expect(response.hits[0].document.deeply.nested.hello).toBe("world")
64 | })
65 |
66 | it("should store log lines in bulk", async () => {
67 | const transport = pinoramaTransport({ url: pinoramaServerUrl })
68 | const log = pino(transport)
69 |
70 | // act
71 | log.info("hello world")
72 | log.info("hello world")
73 | log.info("hello world")
74 | log.info("hello world")
75 | log.info("hello world")
76 |
77 | setImmediate(() => transport.end())
78 | await setTimeout(100)
79 |
80 | const response = await pinoramaClientInstance.search({})
81 | expect(response.hits.length).toBe(5)
82 | for (const hit of response.hits) {
83 | expect(hit.document.msg).toBe("hello world")
84 | }
85 | })
86 |
87 | it("should ignore all values except non-empty plain objects", async () => {
88 | const transport = pinoramaTransport({ url: pinoramaServerUrl })
89 |
90 | // act
91 | transport.write("true\n")
92 | transport.write("null\n")
93 | transport.write("12\n")
94 | transport.write("{}\n")
95 |
96 | setImmediate(() => transport.end())
97 | await setTimeout(100)
98 |
99 | const response = await pinoramaClientInstance.search({})
100 | expect(response.hits.length).toBe(0)
101 | })
102 | })
103 |
--------------------------------------------------------------------------------
/packages/pinorama-transport/src/lib.mts:
--------------------------------------------------------------------------------
1 | import { setInterval } from "node:timers"
2 | import abstractTransport from "pino-abstract-transport"
3 | import { PinoramaClient } from "pinorama-client/node"
4 |
5 | import type { Transform } from "node:stream"
6 | import type { PinoramaClientOptions } from "pinorama-client/node"
7 | import type { BaseOramaPinorama, PinoramaDocument } from "pinorama-types"
8 |
9 | type BulkOptions = {
10 | batchSize: number
11 | flushInterval: number
12 | }
13 |
14 | export const defaultBulkOptions: BulkOptions = {
15 | batchSize: 100,
16 | flushInterval: 5000
17 | }
18 |
19 | export type PinoramaTransportOptions = PinoramaClientOptions & BulkOptions
20 |
21 | /**
22 | * Creates a pino transport that sends logs to a Pinorama server.
23 | *
24 | * @param {Partial} options - Optional settings overrides.
25 | * @returns {Transform} Configured transport instance.
26 | */
27 | export default function pinoramaTransport(
28 | options: Partial
29 | ): Transform {
30 | const clientOpts = filterOptions(options, [
31 | "url",
32 | "maxRetries",
33 | "backoff",
34 | "backoffFactor",
35 | "backoffMax",
36 | "adminSecret"
37 | ])
38 |
39 | const bulkOpts = filterOptions(options, ["batchSize", "flushInterval"])
40 |
41 | const client = new PinoramaClient(clientOpts)
42 |
43 | /* build */
44 | const buildFn = async (stream: Transform) => {
45 | const buffer: PinoramaDocument[] = []
46 | let flushing = false
47 |
48 | const opts: BulkOptions = {
49 | batchSize: bulkOpts?.batchSize ?? defaultBulkOptions.batchSize,
50 | flushInterval: bulkOpts?.flushInterval ?? defaultBulkOptions.flushInterval
51 | }
52 |
53 | const flush = async () => {
54 | if (buffer.length === 0 || flushing) return
55 | flushing = true
56 |
57 | try {
58 | stream.pause()
59 | await client.insert(buffer)
60 | buffer.length = 0
61 | } catch (error) {
62 | console.error("Failed to flush logs:", error)
63 | } finally {
64 | stream.resume()
65 | flushing = false
66 | }
67 | }
68 |
69 | const intervalId = setInterval(() => {
70 | flush()
71 | }, opts.flushInterval)
72 |
73 | stream.on("data", async (data) => {
74 | buffer.push(data)
75 | if (buffer.length >= opts.batchSize) {
76 | await flush()
77 | }
78 | })
79 |
80 | stream.on("end", async () => {
81 | clearInterval(intervalId)
82 | await flush()
83 | })
84 | }
85 |
86 | /* parseLine */
87 | const parseLineFn = (line: string) => {
88 | const obj = JSON.parse(line)
89 |
90 | if (Object.prototype.toString.call(obj) !== "[object Object]") {
91 | throw new Error("not a plain object.")
92 | }
93 |
94 | if (Object.keys(obj).length === 0) {
95 | throw new Error("object is empty.")
96 | }
97 |
98 | return obj
99 | }
100 |
101 | return abstractTransport(buildFn, { parseLine: parseLineFn })
102 | }
103 |
104 | /**
105 | * Filters and returns options specified by keys.
106 | */
107 | export function filterOptions(
108 | options: Partial,
109 | keys: (keyof T)[]
110 | ): Partial | undefined {
111 | let result: Partial | undefined
112 | for (const key of keys) {
113 | if (key in options) {
114 | result = result || {}
115 | result[key] = options[key]
116 | }
117 | }
118 | return result
119 | }
120 |
--------------------------------------------------------------------------------
/packages/pinorama-presets/src/presets/fastify.mts:
--------------------------------------------------------------------------------
1 | import { createPreset } from "../utils.mjs"
2 |
3 | export const fastify = createPreset(
4 | {
5 | time: "number",
6 | level: "enum",
7 | msg: "string",
8 | pid: "enum",
9 | hostname: "string",
10 | reqId: "string",
11 | req: {
12 | method: "string",
13 | url: "string",
14 | hostname: "string",
15 | remoteAddress: "string",
16 | remotePort: "enum"
17 | },
18 | res: {
19 | statusCode: "enum"
20 | },
21 | responseTime: "number"
22 | },
23 | {
24 | facets: {
25 | level: "enum",
26 | msg: "string",
27 | "req.method": "string",
28 | "req.url": "string",
29 | "res.statusCode": "enum",
30 | "req.hostname": "string",
31 | "req.remoteAddress": "string",
32 | "req.remotePort": "enum",
33 | pid: "enum",
34 | hostname: "string"
35 | },
36 | columns: {
37 | time: { visible: true, size: 150 }, // pino
38 | level: { visible: true, size: 70 }, // pino
39 | msg: { visible: true, size: 380 }, // pino
40 | reqId: { visible: true, size: 80 },
41 | "req.method": { visible: true, size: 80 },
42 | "req.url": { visible: true, size: 100 },
43 | "res.statusCode": { visible: true, size: 70 },
44 | "req.hostname": { visible: false, size: 150 },
45 | "req.remoteAddress": { visible: false, size: 100 },
46 | "req.remotePort": { visible: false, size: 80 },
47 | responseTime: { visible: false, size: 150 },
48 | pid: { visible: false, size: 70 }, // pino
49 | hostname: { visible: false, size: 150 } // pino
50 | },
51 | labels: {
52 | level: [
53 | "Level",
54 | {
55 | 10: "TRACE",
56 | 20: "DEBUG",
57 | 30: "INFO",
58 | 40: "WARN",
59 | 50: "ERROR",
60 | 60: "FATAL"
61 | }
62 | ],
63 | time: "Time",
64 | msg: "Message",
65 | pid: "PID",
66 | hostname: "Host",
67 | reqId: "Req. ID",
68 | "req.method": [
69 | "Method",
70 | {
71 | GET: "GET",
72 | POST: "POST",
73 | PUT: "PUT",
74 | PATCH: "PATCH",
75 | DELETE: "DELETE",
76 | HEAD: "HEAD",
77 | OPTIONS: "OPTIONS"
78 | }
79 | ],
80 | "req.url": "URL",
81 | "req.hostname": "Req. Host",
82 | "req.remoteAddress": "Address",
83 | "req.remotePort": "Port",
84 | "res.statusCode": "Status",
85 | responseTime: "Res. Time"
86 | },
87 | formatters: {
88 | time: "timestamp"
89 | },
90 | styles: {
91 | time: {
92 | opacity: "0.5"
93 | },
94 | level: [
95 | {},
96 | {
97 | 10: { color: "var(--color-gray-500)" },
98 | 20: { color: "var(--color-purple-500)" },
99 | 30: { color: "var(--color-lime-500)" },
100 | 40: { color: "var(--color-yellow-500)" },
101 | 50: { color: "var(--color-red-500)" },
102 | 60: { color: "var(--color-red-500)" }
103 | }
104 | ],
105 | "req.method": [
106 | {},
107 | {
108 | GET: { color: "var(--color-blue-500)" },
109 | POST: { color: "var(--color-green-500)" },
110 | PUT: { color: "var(--color-yellow-500)" },
111 | PATCH: { color: "var(--color-orange-500)" },
112 | DELETE: { color: "var(--color-red-500)" },
113 | HEAD: { color: "var(--color-purple-500)" },
114 | OPTIONS: { color: "var(--color-cyan-500)" }
115 | }
116 | ]
117 | }
118 | }
119 | )
120 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | // const Table = React.forwardRef<
6 | // HTMLTableElement,
7 | // React.HTMLAttributes
8 | // >(({ className, ...props }, ref) => (
9 | //
16 | // ))
17 | const Table = React.forwardRef<
18 | HTMLTableElement,
19 | React.HTMLAttributes
20 | >(({ className, ...props }, ref) => (
21 |
26 | ))
27 | Table.displayName = "Table"
28 |
29 | const TableHeader = React.forwardRef<
30 | HTMLTableSectionElement,
31 | React.HTMLAttributes
32 | >(({ className, ...props }, ref) => (
33 |
34 | ))
35 | TableHeader.displayName = "TableHeader"
36 |
37 | const TableBody = React.forwardRef<
38 | HTMLTableSectionElement,
39 | React.HTMLAttributes
40 | >(({ className, ...props }, ref) => (
41 |
46 | ))
47 | TableBody.displayName = "TableBody"
48 |
49 | const TableFooter = React.forwardRef<
50 | HTMLTableSectionElement,
51 | React.HTMLAttributes
52 | >(({ className, ...props }, ref) => (
53 | tr]:last:border-b-0",
57 | className
58 | )}
59 | {...props}
60 | />
61 | ))
62 | TableFooter.displayName = "TableFooter"
63 |
64 | const TableRow = React.forwardRef<
65 | HTMLTableRowElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
76 | ))
77 | TableRow.displayName = "TableRow"
78 |
79 | const TableHead = React.forwardRef<
80 | HTMLTableCellElement,
81 | React.ThHTMLAttributes
82 | >(({ className, ...props }, ref) => (
83 | |
91 | ))
92 | TableHead.displayName = "TableHead"
93 |
94 | const TableCell = React.forwardRef<
95 | HTMLTableCellElement,
96 | React.TdHTMLAttributes
97 | >(({ className, ...props }, ref) => (
98 | |
103 | ))
104 | TableCell.displayName = "TableCell"
105 |
106 | const TableCaption = React.forwardRef<
107 | HTMLTableCaptionElement,
108 | React.HTMLAttributes
109 | >(({ className, ...props }, ref) => (
110 |
115 | ))
116 | TableCaption.displayName = "TableCaption"
117 |
118 | export {
119 | Table,
120 | TableHeader,
121 | TableBody,
122 | TableFooter,
123 | TableHead,
124 | TableRow,
125 | TableCell,
126 | TableCaption
127 | }
128 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-viewer/components/header/header.tsx:
--------------------------------------------------------------------------------
1 | import { SearchInput } from "@/components/search-input"
2 | import { useIntl } from "react-intl"
3 | import { ToggleLiveButton } from "./toggle-live-button"
4 |
5 | import { IconButton } from "@/components/icon-button/icon-button"
6 | import { useModuleHotkeys } from "@/hooks/use-module-hotkeys"
7 | import LogExplorerModule from "@/modules/log-explorer"
8 | import type { AnySchema } from "@orama/orama"
9 | import type { Table } from "@tanstack/react-table"
10 | import {
11 | FilterIcon,
12 | FilterXIcon,
13 | PanelRightIcon,
14 | RefreshCwIcon
15 | } from "lucide-react"
16 | import type { PinoramaIntrospection } from "pinorama-types"
17 | import { ToggleColumnsButton } from "./toggle-columns-button"
18 |
19 | type LogViewerHeaderProps = {
20 | searchInputRef: React.RefObject
21 | introspection: PinoramaIntrospection
22 | table: Table
23 | searchText: string
24 | showClearFiltersButton: boolean
25 | liveMode: boolean
26 | isLoading: boolean
27 | onSearchTextChange: (text: string) => void
28 | onToggleFiltersButtonClick: () => void
29 | onClearFiltersButtonClick: () => void
30 | onToggleLiveButtonClick: (live: boolean) => void
31 | onRefreshButtonClick: () => void
32 | onToggleDetailsButtonClick: () => void
33 | }
34 |
35 | export function LogViewerHeader(props: LogViewerHeaderProps) {
36 | const intl = useIntl()
37 | const moduleHotkeys = useModuleHotkeys(LogExplorerModule)
38 |
39 | const hotkeys = {
40 | showFilters: moduleHotkeys.getHotkey("showFilters"),
41 | refresh: moduleHotkeys.getHotkey("refresh"),
42 | clearFilters: moduleHotkeys.getHotkey("clearFilters"),
43 | showDetails: moduleHotkeys.getHotkey("showDetails")
44 | }
45 |
46 | return (
47 |
48 |
55 |
62 |
66 |
74 | {props.showClearFiltersButton ? (
75 |
82 | ) : null}
83 |
87 |
94 |
95 | )
96 | }
97 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/title-bar/components/connection-status-button.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import {
5 | Form,
6 | FormControl,
7 | FormField,
8 | FormItem,
9 | FormLabel,
10 | FormMessage
11 | } from "@/components/ui/form"
12 | import { Input } from "@/components/ui/input"
13 | import {
14 | Popover,
15 | PopoverContent,
16 | PopoverTrigger
17 | } from "@/components/ui/popover"
18 | import { useAppConfig } from "@/contexts"
19 | import { ConnectionStatus, usePinoramaConnection } from "@/hooks"
20 | import { zodResolver } from "@hookform/resolvers/zod"
21 | import { useForm } from "react-hook-form"
22 | import { FormattedMessage } from "react-intl"
23 | import { z } from "zod"
24 |
25 | const formSchema = z.object({
26 | connectionUrl: z.string().url("Invalid URL")
27 | })
28 |
29 | const STATUS_COLOR: Record = {
30 | [ConnectionStatus.Disconnected]: "bg-gray-500",
31 | [ConnectionStatus.Connecting]: "bg-orange-500",
32 | [ConnectionStatus.Connected]: "bg-green-500",
33 | [ConnectionStatus.Failed]: "bg-red-500",
34 | [ConnectionStatus.Unknown]: "bg-gray-500"
35 | }
36 |
37 | export function ConnectionStatusButton() {
38 | const appConfig = useAppConfig()
39 | const { connectionStatus } = usePinoramaConnection()
40 |
41 | const [open, setOpen] = useState(false)
42 |
43 | const form = useForm>({
44 | resolver: zodResolver(formSchema),
45 | defaultValues: {
46 | connectionUrl: appConfig?.config.connectionUrl || ""
47 | }
48 | })
49 |
50 | const onSubmit = (values: z.infer) => {
51 | appConfig?.setConfig({
52 | ...appConfig.config,
53 | connectionUrl: values.connectionUrl
54 | })
55 |
56 | setOpen(false)
57 | }
58 |
59 | return (
60 |
61 |
62 |
67 |
70 |
71 |
72 |
73 |
74 | {appConfig?.config.connectionUrl ?? "Unknown"}
75 |
76 |
77 |
78 |
79 |
111 |
112 |
113 |
114 | )
115 | }
116 |
--------------------------------------------------------------------------------
/packages/pinorama-transport/src/cli.mts:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | import { readFileSync } from "node:fs"
4 | import type { Transform } from "node:stream"
5 | import { pipeline } from "node:stream/promises"
6 | import { fileURLToPath } from "node:url"
7 | import minimist from "minimist"
8 | import { defaultClientOptions } from "pinorama-client"
9 | import pinoramaTransport, { defaultBulkOptions } from "./lib.mjs"
10 | import type { PinoramaTransportOptions } from "./lib.mjs"
11 |
12 | type PinoramaCliOptions = PinoramaTransportOptions & {
13 | version: string
14 | help: string
15 | }
16 |
17 | /**
18 | * Entry function to start the CLI application.
19 | *
20 | * @param {Partial} argv - The set of CLI options.
21 | */
22 | async function main(argv: Partial) {
23 | const pj = fileURLToPath(new URL("../package.json", import.meta.url))
24 | const { version } = JSON.parse(readFileSync(pj, "utf8"))
25 |
26 | if (argv.help) {
27 | console.log(`
28 | pino-pinorama v${version}
29 |
30 | Description:
31 | A tool for send pino logs to Pinorama server.
32 |
33 | Usage:
34 | pino-pinorama [options]
35 |
36 | Options:
37 | -h, --help Display this help message and exit.
38 | -v, --version Show application version.
39 | -u, --url Set Pinorama server URL.
40 | -k, --adminSecret Secret key for authentication.
41 | -b, --batchSize Define logs per bulk insert (default: ${defaultBulkOptions.batchSize}).
42 | -f, --flushInterval Set flush wait time in ms (default: ${defaultBulkOptions.flushInterval}).
43 | -m, --maxRetries Max retry attempts for requests (default: ${defaultClientOptions.maxRetries}).
44 | -i, --backoff Initial backoff time in ms for retries (default: ${defaultClientOptions.backoff}).
45 | -d, --backoffFactor Backoff factor for exponential increase (default: ${defaultClientOptions.backoffFactor}).
46 | -x, --backoffMax Maximum backoff time in ms (default: ${defaultClientOptions.backoffMax}).
47 |
48 | Example:
49 | cat logs | pino-pinorama --url http://localhost:6200
50 | `)
51 | process.exit()
52 | }
53 |
54 | if (argv.version) {
55 | console.log(version)
56 | process.exit()
57 | }
58 |
59 | if (!argv.url) {
60 | console.error(
61 | "Error: missing required argument '--url'.\n" +
62 | "For more information, use the '--help' argument."
63 | )
64 | process.exit(1)
65 | }
66 |
67 | let transport: Transform
68 |
69 | try {
70 | transport = pinoramaTransport({
71 | url: argv.url,
72 | adminSecret: argv.adminSecret,
73 | batchSize: argv.batchSize,
74 | flushInterval: argv.flushInterval,
75 | maxRetries: argv.maxRetries,
76 | backoff: argv.backoff,
77 | backoffFactor: argv.backoffFactor,
78 | backoffMax: argv.backoffMax
79 | })
80 | } catch (e: any) {
81 | console.error(`Error: ${e.message}`)
82 | process.exit(1)
83 | }
84 |
85 | transport.on("error", (error) => {
86 | console.error("pinoramaTransport error:", error)
87 | })
88 |
89 | pipeline(process.stdin, transport)
90 | }
91 |
92 | const cliOptions = minimist(process.argv.slice(2), {
93 | alias: {
94 | v: "version",
95 | h: "help",
96 | k: "adminSecret",
97 | u: "url",
98 | b: "batchSize",
99 | f: "flushInterval",
100 | m: "maxRetries",
101 | i: "backoff",
102 | d: "backoffFactor",
103 | x: "backoffMax"
104 | },
105 | default: {
106 | batchSize: defaultBulkOptions.batchSize,
107 | flushInterval: defaultBulkOptions.flushInterval,
108 | maxRetries: defaultClientOptions.maxRetries,
109 | backoff: defaultClientOptions.backoff,
110 | backoffFactor: defaultClientOptions.backoffFactor,
111 | backoffMax: defaultClientOptions.backoffMax
112 | },
113 | boolean: ["version", "help"],
114 | string: ["adminSecret", "url"]
115 | })
116 |
117 | main(cliOptions)
118 |
--------------------------------------------------------------------------------
/packages/pinorama-docs/guide/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | outline: deep
3 | ---
4 |
5 | # Overview
6 |
7 | **Pinorama** is a suite of tools designed to help store, explore, and analyze logs from Node.js applications, specifically those generated by [Pino Logger](http://getpino.io/). It's built for development environments, offering flexible options for both real-time and post-mortem log analysis.
8 |
9 | ::: tip
10 | **Need a Quick Start?** 🚀
11 |
12 | If you're looking to get started quickly, check out [Pinorama Studio Quickstart](/guide/quick-start/), a Web App and CLI that allows you to store, view, filter, and analyze logs with minimal setup.
13 | :::
14 |
15 | ## Understanding the suite
16 |
17 | Pinorama is made up of 4 key components, each with a specific role:
18 |
19 | ### 1. Pinorama Studio ⭐
20 |
21 | If you’re looking to get started quickly, Pinorama Studio is the tool for you. It’s a web app and CLI that allows you to store, view, filter, and analyze logs with minimal setup. It works only with Pino structured JSON format logs. Studio can also function as a server when launched with the `--server` option, making it a ideal for scenarios where you want to both receive and view logs in real-time like development environments or debugging sessions.
22 |
23 | ### 2. Pinorama Client
24 |
25 | An isomorphic library that works in both Node.js and the Browser. It’s essentially an HTTP client that mirrors [Orama Search’s API](https://docs.orama.com/open-source/usage/search/introduction), allowing you to interact with an Orama instance through Pinorama Server.
26 | However, this tool is typically used internally by Studio and Transport, so you probably won’t need to interact with it directly unless you’re building a custom solution.
27 |
28 | ### 3. Pinorama Server
29 |
30 | A web server built on Fastify that exposes RESTful endpoints for storing and retrieving logs via [Orama](https://askorama.ai/). It's ideal for cases where you need to index logs with custom structures, customize log line styles (such as colors and formatters), or set up a server to collect and expose logs for real-time access through `pinorama-studio`. Pinorama Server can be deployed as a standalone application or integrated into existing projects as a Fastify plugin.
31 |
32 | ### 4. Pinorama Transport
33 |
34 | Pinorama Transport is used primarily as a [transport](https://getpino.io/#/docs/transports) layer for Pino Logger. It can be integrated programmatically into your Node.js applications to stream logs to a Pinorama Server, or used via CLI to send logs from the terminal using piped output. This flexibility makes it ideal for both automated logging and manual log transmission from processes.
35 |
36 | ## How it works
37 |
38 | Here’s a quick look at how the different components of Pinorama fit together:
39 |
40 | 1. **Generate Logs (Pino Logger):**
41 | - **Pino Logger** creates logs in a structured JSON format as your Node.js application runs. These logs capture essential events and data, forming the basis for further processing and analysis.
42 |
43 | 2. **Stream Logs (Pinorama Transport with Pinorama Client):**
44 | - **Pinorama Transport** streams these logs to **Pinorama Server**. This can be done programmatically within your application or via CLI using piped output. **Pinorama Client** is used internally by Transport to send HTTP requests to **Pinorama Server**'s RESTful endpoints.
45 |
46 | 3. **Store and Index Logs (Pinorama Server):**
47 | - **Pinorama Server** receives the logs from **Pinorama Transport**. It stores and indexes these logs in memory using **Orama Search**, making them available for retrieval and analysis through its RESTful API.
48 |
49 | 4. **Explore Logs (Pinorama Studio with Pinorama Client):**
50 | - **Pinorama Studio** allows you to explore the stored logs through a web-based user interface. It connects to **Pinorama Server** using **Pinorama Client**, enabling real-time log viewing, filtering, and analysis.
51 |
52 | ## When to use
53 |
54 | ::: tip
55 | **For Development:** Pinorama is perfect for local development, offering tools for both live log streaming and detailed post-mortem analysis.
56 | :::
57 |
58 | ::: danger
59 | **Avoid Production!** Pinorama Server is not recommended for production environments due to its in-memory log storage.
60 | :::
61 |
--------------------------------------------------------------------------------
/packages/pinorama-docs/.vitepress/config/en.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitepress"
2 | import { version } from "../../../pinorama-studio/package.json"
3 |
4 | export const en = defineConfig({
5 | lang: "en-US",
6 |
7 | themeConfig: {
8 | nav: [
9 | {
10 | text: "Guide",
11 | link: "/guide/",
12 | activeMatch: "/guide/"
13 | },
14 | {
15 | text: "Studio",
16 | link: "/guide/pinorama-studio/",
17 | activeMatch: "/guide/pinorama-studio/"
18 | },
19 | {
20 | text: "Client",
21 | link: "/guide/pinorama-client/",
22 | activeMatch: "/guide/pinorama-client/"
23 | },
24 | {
25 | text: "Server",
26 | link: "/guide/pinorama-server/",
27 | activeMatch: "/guide/pinorama-server/"
28 | },
29 | {
30 | text: "Transport",
31 | link: "/guide/pinorama-transport/",
32 | activeMatch: "/guide/pinorama-transport/"
33 | },
34 | {
35 | text: `v${version}`,
36 | items: [
37 | {
38 | text: `v${version} (studio)`,
39 | link: "/"
40 | },
41 | {
42 | text: "Release Notes",
43 | link: "https://github.com/pinoramajs/pinorama/releases"
44 | }
45 | ]
46 | }
47 | ],
48 |
49 | sidebar: [
50 | {
51 | text: "Guide",
52 | items: [
53 | {
54 | text: "Overview",
55 | link: "/guide/"
56 | },
57 | {
58 | text: "Quick Start",
59 | link: "/guide/quick-start"
60 | },
61 | {
62 | text: "Use Cases",
63 | link: "/guide/use-cases"
64 | }
65 | ]
66 | },
67 | {
68 | text: "Studio ⭐",
69 | items: [
70 | {
71 | text: "Overview",
72 | link: "/pinorama-studio"
73 | },
74 | {
75 | text: "Installation",
76 | link: "/pinorama-studio/installation"
77 | },
78 | {
79 | text: "Usage",
80 | link: "/pinorama-studio/usage"
81 | },
82 | {
83 | text: "CLI",
84 | link: "/pinorama-studio/api"
85 | }
86 | ]
87 | },
88 | {
89 | text: "Client",
90 | items: [
91 | {
92 | text: "Overview",
93 | link: "/pinorama-client"
94 | },
95 | {
96 | text: "Installation",
97 | link: "/pinorama-client/installation"
98 | },
99 | {
100 | text: "Usage",
101 | link: "/pinorama-client/usage"
102 | },
103 | {
104 | text: "API Reference",
105 | link: "/pinorama-client/api"
106 | }
107 | ]
108 | },
109 | {
110 | text: "Server",
111 | items: [
112 | {
113 | text: "Overview",
114 | link: "/pinorama-server"
115 | },
116 | {
117 | text: "Installation",
118 | link: "/pinorama-server/installation"
119 | },
120 | {
121 | text: "Configuration",
122 | link: "/pinorama-server/configuration"
123 | },
124 | {
125 | text: "API Reference",
126 | link: "/pinorama-server/api"
127 | }
128 | ]
129 | },
130 | {
131 | text: "Transport",
132 | items: [
133 | {
134 | text: "Overview",
135 | link: "/pinorama-transport"
136 | },
137 | {
138 | text: "Installation",
139 | link: "/pinorama-transport/installation"
140 | },
141 | {
142 | text: "Usage",
143 | link: "/pinorama-transport/usage"
144 | }
145 | ]
146 | }
147 | ],
148 |
149 | footer: {
150 | message: "Open Source ❤ MIT Licensed",
151 | copyright: "© 2024 Francesco Pasqua & Contributors"
152 | },
153 |
154 | docFooter: {
155 | prev: "Previous page",
156 | next: "Next page"
157 | }
158 | }
159 | })
160 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/modules/log-explorer/components/log-filters/components/facet.tsx:
--------------------------------------------------------------------------------
1 | import { EmptyStateInline } from "@/components/empty-state/empty-state"
2 | import { ErrorState } from "@/components/error-state/error-state"
3 | import { useCallback, useMemo, useState } from "react"
4 | import { useIntl } from "react-intl"
5 | import { useFacet } from "../hooks/use-facet"
6 | import { facetFilterOperationsFactory } from "../lib/operations"
7 | import { FacetBody } from "./facet-body"
8 | import { FacetHeader } from "./facet-header"
9 |
10 | import type { AnySchema } from "@orama/orama"
11 | import type { IntrospectionFacet, PinoramaIntrospection } from "pinorama-types"
12 | import type { FacetFilter, FacetValue, SearchFilters } from "../types"
13 |
14 | type FacetProps = {
15 | introspection: PinoramaIntrospection
16 | name: string
17 | type: IntrospectionFacet
18 | searchText: string
19 | filters: SearchFilters
20 | liveMode: boolean
21 | onFiltersChange: (filters: SearchFilters) => void
22 | }
23 |
24 | export function Facet(props: FacetProps) {
25 | const intl = useIntl()
26 |
27 | const [open, setOpen] = useState(true)
28 |
29 | const {
30 | data: facet,
31 | fetchStatus,
32 | status,
33 | error
34 | } = useFacet(props.name, props.searchText, props.filters, props.liveMode)
35 |
36 | const operations: any = facetFilterOperationsFactory(props.type)
37 | const criteria = props.filters[props.name] || operations.create()
38 | const selelectedOptionCount = operations.length(criteria)
39 |
40 | const handleReset = useCallback(
41 | (event: React.MouseEvent) => {
42 | event.stopPropagation()
43 | const filters = { ...props.filters }
44 | delete filters[props.name]
45 | props.onFiltersChange(filters)
46 | },
47 | [props.onFiltersChange, props.name, props.filters]
48 | )
49 |
50 | const selectedValuesNotInDataSource = useMemo(() => {
51 | const selectedItems: FacetValue[] = operations
52 | .values(props.filters[props.name] as FacetFilter)
53 | .map((value: string | number) => {
54 | return {
55 | value,
56 | count: facet?.values[value] || 0
57 | }
58 | })
59 |
60 | return selectedItems.filter(
61 | (item) => !(item.value in (facet?.values || {}))
62 | )
63 | }, [props.filters, props.name, facet?.values, operations])
64 |
65 | const allValues = useMemo(() => {
66 | const currentValues = Object.entries(facet?.values || {})
67 | .filter(([value]) => value !== "") // NOTE: Don't show empty values
68 | .map(([value, count]) => {
69 | // NOTE: If the value is a number of type string,
70 | // we need to parse it to a number.
71 | let parsedValue: string | number = value
72 | if (props.type === "enum" && Number.isFinite(+value)) {
73 | parsedValue = Number(value)
74 | }
75 | return { value: parsedValue, count }
76 | })
77 |
78 | return [...selectedValuesNotInDataSource, ...currentValues]
79 | }, [selectedValuesNotInDataSource, facet?.values, props.type])
80 |
81 | const hasError = status === "error"
82 | const hasNoData = allValues.length === 0
83 |
84 | return (
85 |
86 | setOpen((value) => !value)}
93 | onCountClick={handleReset}
94 | />
95 | {open ? (
96 | hasError ? (
97 |
98 | ) : hasNoData ? (
99 |
103 | ) : (
104 |
112 | )
113 | ) : null}
114 |
115 | )
116 | }
117 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as DialogPrimitive from "@radix-ui/react-dialog"
2 | import { X } from "lucide-react"
3 | import * as React from "react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Dialog = DialogPrimitive.Root
8 |
9 | const DialogTrigger = DialogPrimitive.Trigger
10 |
11 | const DialogPortal = DialogPrimitive.Portal
12 |
13 | const DialogClose = DialogPrimitive.Close
14 |
15 | const DialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
29 |
30 | const DialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, children, ...props }, ref) => (
34 |
35 |
36 |
44 | {children}
45 |
46 |
47 | Close
48 |
49 |
50 |
51 | ))
52 | DialogContent.displayName = DialogPrimitive.Content.displayName
53 |
54 | const DialogHeader = ({
55 | className,
56 | ...props
57 | }: React.HTMLAttributes) => (
58 |
65 | )
66 | DialogHeader.displayName = "DialogHeader"
67 |
68 | const DialogFooter = ({
69 | className,
70 | ...props
71 | }: React.HTMLAttributes) => (
72 |
79 | )
80 | DialogFooter.displayName = "DialogFooter"
81 |
82 | const DialogTitle = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
94 | ))
95 | DialogTitle.displayName = DialogPrimitive.Title.displayName
96 |
97 | const DialogDescription = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ))
107 | DialogDescription.displayName = DialogPrimitive.Description.displayName
108 |
109 | export {
110 | Dialog,
111 | DialogPortal,
112 | DialogOverlay,
113 | DialogClose,
114 | DialogTrigger,
115 | DialogContent,
116 | DialogHeader,
117 | DialogFooter,
118 | DialogTitle,
119 | DialogDescription
120 | }
121 |
--------------------------------------------------------------------------------
/packages/pinorama-studio/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import type * as LabelPrimitive from "@radix-ui/react-label"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import * as React from "react"
4 | import {
5 | Controller,
6 | type ControllerProps,
7 | type FieldPath,
8 | type FieldValues,
9 | FormProvider,
10 | useFormContext
11 | } from "react-hook-form"
12 |
13 | import { Label } from "@/components/ui/label"
14 | import { cn } from "@/lib/utils"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField
176 | }
177 |
--------------------------------------------------------------------------------