@bluemarble/bi 0.6.5 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DashboardDevtools-CMIuUYpw.js → DashboardDevtools-D0pzwSgn.js} +527 -240
- package/dist/DashboardDevtools-D0pzwSgn.js.map +1 -0
- package/dist/{ModelDictionary-CyeYU6ZJ.js → ModelDictionary-Cw4sa8tW.js} +3 -3
- package/dist/{ModelDictionary-CyeYU6ZJ.js.map → ModelDictionary-Cw4sa8tW.js.map} +1 -1
- package/dist/cli.js +166 -10
- package/dist/cli.js.map +1 -1
- package/dist/client/index.d.ts +49 -7
- package/dist/client/index.js +160 -35
- package/dist/client/index.js.map +1 -1
- package/dist/{layout-DsG-zRIO.d.ts → formatRules-DxA9vMvz.d.ts} +272 -207
- package/dist/{layout-CBzYDoqE.js → layout-BS3nO6OT.js} +225 -13
- package/dist/layout-BS3nO6OT.js.map +1 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/dist/DashboardDevtools-CMIuUYpw.js.map +0 -1
- package/dist/layout-CBzYDoqE.js.map +0 -1
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli/scaffold.ts","../src/cli/index.ts"],"sourcesContent":["import { literal, toModelCode } from '../codegen'\nimport type { ChartsLayout } from '../layout'\n\n/**\n * Arquivos de um dashboard novo: o modelo vazio (`models/<nome>.ts`), o registro dele no\n * `models/index.ts` e o layout vazio (`layouts/<id>.json`). Tudo como texto, sem tocar no disco —\n * quem grava é o CLI (`bm-bi new`).\n */\n\nexport class ScaffoldError extends Error {}\n\nconst IDENTIFIER = /^[A-Za-z_$][\\w$]*$/\nconst LAYOUT_ID = /^[\\w-]+$/\n\nexport function checkModelName(name: string) {\n if (!IDENTIFIER.test(name))\n throw new ScaffoldError(\n `Nome de modelo inválido: \"${name}\" (use um identificador JS, ex.: analiseCusto)`\n )\n}\n\nexport function checkLayoutId(id: string) {\n if (!LAYOUT_ID.test(id))\n throw new ScaffoldError(`Id de layout inválido: \"${id}\" (só letras, números, _ e -)`)\n}\n\n/** Modelo vazio, pronto para montar pelo diagrama da aba Modelo do devtools. */\nexport function modelFileCode(name: string) {\n checkModelName(name)\n return toModelCode({ name, tables: {}, dimensions: {}, measures: {} }).replace(\n 'export const',\n '/** Monte tabelas, relacionamentos, dimensões e medidas pela aba Modelo do devtools. */\\nexport const'\n )\n}\n\n/** Layout vazio, pronto para montar pela aba Layout do devtools. */\nexport function layoutFileCode(id: string) {\n checkLayoutId(id)\n const layout: ChartsLayout = { id, filters: [], charts: [] }\n return `${JSON.stringify(layout, null, 2)}\\n`\n}\n\n/** `models/index.ts` novo, com só este modelo. */\nexport function modelIndexCode(name: string) {\n checkModelName(name)\n return [\n `import { ${name} } from './${name}'`,\n '',\n '/** Modelos expostos pelo endpoint do dashboard (`runQuery(dashboardModels, ...)`). */',\n 'export const dashboardModels = {',\n ` ${name}`,\n '}',\n ''\n ].join('\\n')\n}\n\n/**\n * Acrescenta o modelo ao `models/index.ts` existente: o import depois do último import e o nome no\n * primeiro `export const x = { ... }`. Devolve `null` se o modelo já está registrado.\n */\nexport function registerModel(source: string, name: string): string | null {\n checkModelName(name)\n const importPath = new RegExp(`from\\\\s+['\"]\\\\./${name}(\\\\.ts)?['\"]`)\n if (importPath.test(source)) return null\n\n const object = /export\\s+const\\s+\\w+(?:\\s*:[^=]+)?\\s*=\\s*\\{/.exec(source)\n if (!object)\n throw new ScaffoldError(\n 'Não encontrei `export const <nome> = { ... }` no models/index.ts — registre o modelo à mão.'\n )\n const open = object.index + object[0].length - 1\n let depth = 0\n let close = -1\n for (let i = open; i < source.length; i++) {\n if (source[i] === '{') depth++\n else if (source[i] === '}' && --depth === 0) {\n close = i\n break\n }\n }\n if (close === -1) throw new ScaffoldError('Objeto de modelos do models/index.ts sem `}`.')\n\n const body = source.slice(open + 1, close)\n const items = body.trimEnd()\n let newBody: string\n if (!items.trim()) newBody = `\\n ${name}\\n`\n else if (!body.includes('\\n')) newBody = ` ${items.trim().replace(/,$/, '')}, ${name} `\n else newBody = `${items.endsWith(',') ? items : `${items},`}\\n ${name}\\n`\n let result = `${source.slice(0, open + 1)}${newBody}${source.slice(close)}`\n\n // Import logo depois do último import do arquivo (ou no topo, se não houver)\n const line = `import { ${name} } from './${name}'\\n`\n const imports = [...result.matchAll(/^import[\\s\\S]*?from\\s+['\"][^'\"]+['\"];?[^\\n]*\\n/gm)]\n const last = imports.at(-1)\n const at = last ? last.index + last[0].length : 0\n result = `${result.slice(0, at)}${line}${last ? '' : '\\n'}${result.slice(at)}`\n return result\n}\n\n// --- página ---\n\n/** `analiseCusto` → `analise-custo` (rota da página). */\nexport const kebabCase = (text: string) =>\n text\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/[_\\s]+/g, '-')\n .toLowerCase()\n\n/** `analiseCusto` → `Analise Custo` (título padrão da página). */\nexport const titleCase = (text: string) =>\n kebabCase(text)\n .split('-')\n .filter(Boolean)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ')\n\n/** `analiseCusto` → `AnaliseCustoPage` (nome do componente). */\nexport const pageComponentName = (text: string) => `${titleCase(text).replaceAll(' ', '')}Page`\n\nexport type PageOptions = {\n model: string\n /** Import do JSON do layout, relativo à página (ex.: `../../dashboard/layouts/vendas.json`). */\n layoutImport: string\n title: string\n /** Endpoint do `runQuery` (padrão `/api/dashboard/query`). */\n endpoint: string\n /** App Router do Next: a página vira client component (`'use client'`). */\n clientDirective?: boolean\n /** Import do registro de tipos de gráfico do app (`export const charts`), relativo à página. */\n chartsImport?: string\n}\n\n/**\n * Valores trocados num template de página do app (`page.template`): o template é a página de um\n * dashboard com esses marcadores no lugar do que muda de um para outro.\n */\nexport const pageTemplateValues = (options: PageOptions): Record<string, string> => ({\n __MODEL__: options.model,\n __LAYOUT__: options.layoutImport,\n __TITLE__: options.title,\n __COMPONENT__: pageComponentName(options.model),\n __ENDPOINT__: options.endpoint\n})\n\nexport function fillPageTemplate(template: string, options: PageOptions) {\n let content = template\n for (const [marker, value] of Object.entries(pageTemplateValues(options)))\n content = content.replaceAll(marker, value)\n return content\n}\n\n/** Página padrão: provider, filtros e gráficos do layout, consultando o endpoint com `fetch`. */\nexport function pageFileCode(options: PageOptions) {\n const { model, layoutImport, title, endpoint, clientDirective, chartsImport } = options\n if (endpoint.includes(\"'\")) throw new ScaffoldError(`Endpoint inválido: ${endpoint}`)\n checkModelName(model)\n return [\n ...(clientDirective ? [\"'use client'\", ''] : []),\n 'import {',\n ' DashboardCanvas,',\n ' DashboardCharts,',\n ' type DashboardFetcher,',\n ' DashboardFilters,',\n ' DashboardProvider',\n \"} from '@bluemarble/bi/client'\",\n \"import type { ChartsLayout, DashboardResult } from '@bluemarble/bi/types'\",\n \"import { Box, Typography } from '@mui/material'\",\n ...(chartsImport ? [`import { charts } from '${chartsImport}'`] : []),\n `import layout from '${layoutImport}'`,\n '',\n '/**',\n ` * Dashboard do modelo \\`${model}\\`. Filtros, gráficos e o tamanho da tela vêm do layout JSON,`,\n ' * editável pela aba Layout do devtools (◆ BI, em desenvolvimento).',\n ' */',\n '',\n `const TITULO = ${literal(title)}`,\n 'const chartsLayout = layout as ChartsLayout',\n '',\n '// Fora do componente: referência estável, senão toda consulta refaria a cada render',\n 'const fetcher: DashboardFetcher = async (query) => {',\n ` const response = await fetch('${endpoint}', {`,\n \" method: 'POST',\",\n \" headers: { 'Content-Type': 'application/json' },\",\n ' body: JSON.stringify(query)',\n ' })',\n ' if (!response.ok) throw new Error(await response.text())',\n ' return (await response.json()) as DashboardResult',\n '}',\n '',\n `export default function ${pageComponentName(model)}() {`,\n ' return (',\n ` <DashboardProvider model=\"${model}\" fetcher={fetcher}${chartsImport ? ' charts={charts}' : ''}>`,\n ' <Box sx={{ p: 2 }}>',\n ' {/* Com `canvas` no JSON, tela de tamanho fixo que escala; sem, responsivo */}',\n ' <DashboardCanvas layout={chartsLayout}>',\n \" <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>\",\n ' <Typography component=\"h1\" variant=\"h5\">',\n ' {TITULO}',\n ' </Typography>',\n ' <DashboardFilters layout={chartsLayout} />',\n ' <DashboardCharts layout={chartsLayout} />',\n ' </Box>',\n ' </DashboardCanvas>',\n ' </Box>',\n ' </DashboardProvider>',\n ' )',\n '}',\n ''\n ].join('\\n')\n}\n\n/**\n * Extensão das páginas do Pages Router pelo `pageExtensions` do `next.config` (ex.: `page.tsx` no\n * `custo`). Sem a opção (ou sem config), `tsx`.\n */\nexport function pageExtension(nextConfig: string | undefined) {\n const list = /pageExtensions\\s*:\\s*\\[([^\\]]*)\\]/.exec(nextConfig ?? '')?.[1]\n const extensions = [...(list ?? '').matchAll(/['\"]([^'\"]+)['\"]/g)].map((match) => match[1])\n return extensions.find((extension) => extension?.endsWith('tsx')) ?? 'tsx'\n}\n","#!/usr/bin/env node\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { basename, dirname, join, relative, sep } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport {\n checkLayoutId,\n checkModelName,\n fillPageTemplate,\n kebabCase,\n layoutFileCode,\n modelFileCode,\n modelIndexCode,\n type PageOptions,\n pageExtension,\n pageFileCode,\n registerModel,\n ScaffoldError,\n titleCase\n} from './scaffold'\n\nconst HELP = `Uso: bm-bi new <nome> [opções]\n\nCria um dashboard vazio: o modelo (models/<nome>.ts, registrado em models/index.ts), o layout\n(layouts/<id>.json) e a página. Depois é montar tudo pelas abas Modelo e Layout do devtools.\n\nA página vai para o router do Next que existir: src/pages (ou pages) →\ndashboard/<id-em-kebab>/index.<ext>, com a extensão do pageExtensions do next.config; src/app (ou\napp) → dashboard/<id-em-kebab>/page.tsx, com 'use client'. Com <dir>/page.template, a página sai\ndele, trocando __MODEL__, __LAYOUT__ (import do JSON), __TITLE__, __COMPONENT__ e __ENDPOINT__.\n\nOpções:\n --id <id> id do layout (padrão: o nome do modelo)\n --title <texto> título da página (padrão: o id em palavras)\n --dir <pasta> pasta dos dashboards (padrão: src/dashboard)\n --models <pasta> pasta dos modelos (padrão: <dir>/models)\n --layouts <pasta> pasta dos layouts (padrão: <dir>/layouts)\n --page <arquivo> arquivo da página (padrão: pelo router do Next, ver acima)\n --skip-page não cria a página\n --template <arq.> template da página (padrão: <dir>/page.template, se existir)\n --endpoint <url> endpoint do runQuery na página padrão (padrão: /api/dashboard/query)\n -h, --help esta ajuda\n`\n\nconst exists = (path: string) => existsSync(path)\n\n/** Caminho de import de `to` a partir do arquivo `from` (sempre com `./` ou `../` e `/`). */\nfunction importPath(from: string, to: string) {\n const path = relative(dirname(from), to).split(sep).join('/')\n return path.startsWith('.') ? path : `./${path}`\n}\n\n/** Página pelo router do Next que existir no app (Pages primeiro, como o `custo`). */\nfunction defaultPagePath(cwd: string, slug: string) {\n for (const pages of ['src/pages', 'pages']) {\n if (!exists(join(cwd, pages))) continue\n const config = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs']\n .map((file) => join(cwd, file))\n .find(exists)\n const extension = pageExtension(config && readFileSync(config, 'utf8'))\n return join(cwd, pages, 'dashboard', slug, `index.${extension}`)\n }\n for (const app of ['src/app', 'app'])\n if (exists(join(cwd, app))) return join(cwd, app, 'dashboard', slug, 'page.tsx')\n return undefined\n}\n\n/** `export const charts` em <dir>/charts: os tipos de gráfico do app entram no provider. */\nfunction chartsIndex(dir: string) {\n return ['index.ts', 'index.tsx']\n .map((file) => join(dir, 'charts', file))\n .find((path) => exists(path) && /export\\s+const\\s+charts\\b/.test(readFileSync(path, 'utf8')))\n}\n\nfunction main(argv: string[]) {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n id: { type: 'string' },\n dir: { type: 'string', default: 'src/dashboard' },\n models: { type: 'string' },\n layouts: { type: 'string' },\n title: { type: 'string' },\n page: { type: 'string' },\n 'skip-page': { type: 'boolean' },\n template: { type: 'string' },\n endpoint: { type: 'string', default: '/api/dashboard/query' },\n help: { type: 'boolean', short: 'h' }\n }\n })\n const [command, name, ...rest] = positionals\n if (values.help || !command) {\n process.stdout.write(HELP)\n return\n }\n if (command !== 'new') throw new ScaffoldError(`Comando desconhecido: ${command}\\n\\n${HELP}`)\n if (!name) throw new ScaffoldError(`Falta o nome do modelo.\\n\\n${HELP}`)\n if (rest.length) throw new ScaffoldError(`Argumentos a mais: ${rest.join(' ')}`)\n\n const id = values.id ?? name\n checkModelName(name)\n checkLayoutId(id)\n\n const cwd = process.cwd()\n const modelsDir = join(cwd, values.models ?? join(values.dir, 'models'))\n const layoutsDir = join(cwd, values.layouts ?? join(values.dir, 'layouts'))\n const modelPath = join(modelsDir, `${name}.ts`)\n const indexPath = join(modelsDir, 'index.ts')\n const layoutPath = join(layoutsDir, `${id}.json`)\n const dashboardDir = join(cwd, values.dir)\n const pagePath = values['skip-page']\n ? undefined\n : values.page\n ? join(cwd, values.page)\n : defaultPagePath(cwd, kebabCase(id))\n const templatePath = values.template\n ? join(cwd, values.template)\n : [join(dashboardDir, 'page.template')].find(exists)\n if (templatePath && !exists(templatePath))\n throw new ScaffoldError(`Template não encontrado: ${relative(cwd, templatePath)}`)\n\n // Confere tudo antes de escrever: ou cria o dashboard inteiro, ou nada\n for (const path of [modelPath, layoutPath, pagePath])\n if (path && existsSync(path)) throw new ScaffoldError(`Já existe: ${relative(cwd, path)}`)\n const index = existsSync(indexPath)\n ? registerModel(readFileSync(indexPath, 'utf8'), name)\n : modelIndexCode(name)\n let page: string | undefined\n if (pagePath) {\n const charts = chartsIndex(dashboardDir)\n const options: PageOptions = {\n model: name,\n layoutImport: importPath(pagePath, layoutPath),\n title: values.title ?? titleCase(id),\n endpoint: values.endpoint,\n // App Router: a página usa hooks, então é client component\n clientDirective:\n basename(pagePath).startsWith('page.') &&\n relative(cwd, pagePath).split(sep).includes('app'),\n chartsImport: charts && importPath(pagePath, dirname(charts))\n }\n page = templatePath\n ? fillPageTemplate(readFileSync(templatePath, 'utf8'), options)\n : pageFileCode(options)\n }\n\n const written: string[] = []\n const write = (path: string, content: string) => {\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, content)\n written.push(relative(cwd, path))\n }\n write(modelPath, modelFileCode(name))\n if (index !== null) write(indexPath, index)\n write(layoutPath, layoutFileCode(id))\n if (pagePath && page !== undefined) write(pagePath, page)\n\n process.stdout.write(\n [\n ...written.map((path) => `✓ ${path}`),\n ...(pagePath\n ? templatePath\n ? [` (página do template ${relative(cwd, templatePath)})`]\n : []\n : values['skip-page']\n ? []\n : [\n '',\n 'Página não criada: não achei src/pages nem src/app. Use --page <arquivo>, com o layout:',\n ` <DashboardProvider model=\"${name}\" fetcher={fetcher}>`,\n ' <DashboardFilters layout={layout} />',\n ' <DashboardCharts layout={layout} />',\n ' </DashboardProvider>'\n ]),\n ''\n ].join('\\n')\n )\n}\n\ntry {\n main(process.argv.slice(2))\n} catch (error) {\n if (!(error instanceof ScaffoldError) && !(error instanceof TypeError)) throw error\n process.stderr.write(`${error.message}\\n`)\n process.exitCode = 1\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli/scaffold.ts","../src/cli/index.ts"],"sourcesContent":["import { literal, toModelCode } from '../codegen'\nimport type { ChartsLayout } from '../layout'\n\n/**\n * Arquivos de um dashboard novo: o modelo vazio (`models/<nome>.ts`), o registro dele no\n * `models/index.ts` e o layout vazio (`layouts/<id>.json`). Tudo como texto, sem tocar no disco —\n * quem grava é o CLI (`bm-bi new`).\n */\n\nexport class ScaffoldError extends Error {}\n\nconst IDENTIFIER = /^[A-Za-z_$][\\w$]*$/\nconst LAYOUT_ID = /^[\\w-]+$/\n\nexport function checkModelName(name: string) {\n if (!IDENTIFIER.test(name))\n throw new ScaffoldError(\n `Nome de modelo inválido: \"${name}\" (use um identificador JS, ex.: analiseCusto)`\n )\n}\n\nexport function checkLayoutId(id: string) {\n if (!LAYOUT_ID.test(id))\n throw new ScaffoldError(`Id de layout inválido: \"${id}\" (só letras, números, _ e -)`)\n}\n\n/** Modelo vazio, pronto para montar pelo diagrama da aba Modelo do devtools. */\nexport function modelFileCode(name: string) {\n checkModelName(name)\n return toModelCode({ name, tables: {}, dimensions: {}, measures: {} }).replace(\n 'export const',\n '/** Monte tabelas, relacionamentos, dimensões e medidas pela aba Modelo do devtools. */\\nexport const'\n )\n}\n\n/** Layout vazio, pronto para montar pela aba Layout do devtools. */\nexport function layoutFileCode(id: string) {\n checkLayoutId(id)\n const layout: ChartsLayout = { id, filters: [], charts: [] }\n return `${JSON.stringify(layout, null, 2)}\\n`\n}\n\n/** `models/index.ts` novo, com só este modelo. */\nexport function modelIndexCode(name: string) {\n checkModelName(name)\n return [\n `import { ${name} } from './${name}'`,\n '',\n '/** Modelos expostos pelo endpoint do dashboard (`runQuery(dashboardModels, ...)`). */',\n 'export const dashboardModels = {',\n ` ${name}`,\n '}',\n ''\n ].join('\\n')\n}\n\n/**\n * Acrescenta o modelo ao `models/index.ts` existente: o import depois do último import e o nome no\n * primeiro `export const x = { ... }`. Devolve `null` se o modelo já está registrado.\n */\nexport function registerModel(source: string, name: string): string | null {\n checkModelName(name)\n const importPath = new RegExp(`from\\\\s+['\"]\\\\./${name}(\\\\.ts)?['\"]`)\n if (importPath.test(source)) return null\n\n const object = /export\\s+const\\s+\\w+(?:\\s*:[^=]+)?\\s*=\\s*\\{/.exec(source)\n if (!object)\n throw new ScaffoldError(\n 'Não encontrei `export const <nome> = { ... }` no models/index.ts — registre o modelo à mão.'\n )\n const open = object.index + object[0].length - 1\n let depth = 0\n let close = -1\n for (let i = open; i < source.length; i++) {\n if (source[i] === '{') depth++\n else if (source[i] === '}' && --depth === 0) {\n close = i\n break\n }\n }\n if (close === -1) throw new ScaffoldError('Objeto de modelos do models/index.ts sem `}`.')\n\n const body = source.slice(open + 1, close)\n const items = body.trimEnd()\n let newBody: string\n if (!items.trim()) newBody = `\\n ${name}\\n`\n else if (!body.includes('\\n')) newBody = ` ${items.trim().replace(/,$/, '')}, ${name} `\n else newBody = `${items.endsWith(',') ? items : `${items},`}\\n ${name}\\n`\n let result = `${source.slice(0, open + 1)}${newBody}${source.slice(close)}`\n\n // Import logo depois do último import do arquivo (ou no topo, se não houver)\n const line = `import { ${name} } from './${name}'\\n`\n const imports = [...result.matchAll(/^import[\\s\\S]*?from\\s+['\"][^'\"]+['\"];?[^\\n]*\\n/gm)]\n const last = imports.at(-1)\n const at = last ? last.index + last[0].length : 0\n result = `${result.slice(0, at)}${line}${last ? '' : '\\n'}${result.slice(at)}`\n return result\n}\n\n// --- página ---\n\n/** `analiseCusto` → `analise-custo` (rota da página). */\nexport const kebabCase = (text: string) =>\n text\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/[_\\s]+/g, '-')\n .toLowerCase()\n\n/** `analiseCusto` → `Analise Custo` (título padrão da página). */\nexport const titleCase = (text: string) =>\n kebabCase(text)\n .split('-')\n .filter(Boolean)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ')\n\n/** `analiseCusto` → `AnaliseCustoPage` (nome do componente). */\nexport const pageComponentName = (text: string) => `${titleCase(text).replaceAll(' ', '')}Page`\n\nexport type PageOptions = {\n model: string\n /** Import do JSON do layout, relativo à página (ex.: `../../dashboard/layouts/vendas.json`). */\n layoutImport: string\n title: string\n /** Endpoint do `runQuery` (padrão `/api/dashboard/query`). */\n endpoint: string\n /** App Router do Next: a página vira client component (`'use client'`). */\n clientDirective?: boolean\n /** Import do registro de tipos de gráfico do app (`export const charts`), relativo à página. */\n chartsImport?: string\n}\n\n/**\n * Valores trocados num template de página do app (`page.template`): o template é a página de um\n * dashboard com esses marcadores no lugar do que muda de um para outro.\n */\nexport const pageTemplateValues = (options: PageOptions): Record<string, string> => ({\n __MODEL__: options.model,\n __LAYOUT__: options.layoutImport,\n __TITLE__: options.title,\n __COMPONENT__: pageComponentName(options.model),\n __ENDPOINT__: options.endpoint\n})\n\nexport function fillPageTemplate(template: string, options: PageOptions) {\n let content = template\n for (const [marker, value] of Object.entries(pageTemplateValues(options)))\n content = content.replaceAll(marker, value)\n return content\n}\n\n/** Página padrão: provider, filtros e gráficos do layout, consultando o endpoint com `fetch`. */\nexport function pageFileCode(options: PageOptions) {\n const { model, layoutImport, title, endpoint, clientDirective, chartsImport } = options\n if (endpoint.includes(\"'\")) throw new ScaffoldError(`Endpoint inválido: ${endpoint}`)\n checkModelName(model)\n return [\n ...(clientDirective ? [\"'use client'\", ''] : []),\n 'import {',\n ' DashboardCanvas,',\n ' DashboardCharts,',\n ' type DashboardFetcher,',\n ' DashboardFilters,',\n ' DashboardProvider',\n \"} from '@bluemarble/bi/client'\",\n \"import type { ChartsLayout, DashboardResult } from '@bluemarble/bi/types'\",\n \"import { Box, Typography } from '@mui/material'\",\n ...(chartsImport ? [`import { charts } from '${chartsImport}'`] : []),\n `import layout from '${layoutImport}'`,\n '',\n '/**',\n ` * Dashboard do modelo \\`${model}\\`. Filtros, gráficos e o tamanho da tela vêm do layout JSON,`,\n ' * editável pela aba Layout do devtools (◆ BI, em desenvolvimento).',\n ' */',\n '',\n `const TITULO = ${literal(title)}`,\n 'const chartsLayout = layout as ChartsLayout',\n '',\n '// Fora do componente: referência estável, senão toda consulta refaria a cada render',\n 'const fetcher: DashboardFetcher = async (query) => {',\n ` const response = await fetch('${endpoint}', {`,\n \" method: 'POST',\",\n \" headers: { 'Content-Type': 'application/json' },\",\n ' body: JSON.stringify(query)',\n ' })',\n ' if (!response.ok) throw new Error(await response.text())',\n ' return (await response.json()) as DashboardResult',\n '}',\n '',\n `export default function ${pageComponentName(model)}() {`,\n ' return (',\n ` <DashboardProvider model=\"${model}\" fetcher={fetcher}${chartsImport ? ' charts={charts}' : ''}>`,\n ' <Box sx={{ p: 2 }}>',\n ' {/* Com `canvas` no JSON, tela de tamanho fixo que escala; sem, responsivo */}',\n ' <DashboardCanvas layout={chartsLayout}>',\n \" <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>\",\n ' <Typography component=\"h1\" variant=\"h5\">',\n ' {TITULO}',\n ' </Typography>',\n ' <DashboardFilters layout={chartsLayout} />',\n ' <DashboardCharts layout={chartsLayout} />',\n ' </Box>',\n ' </DashboardCanvas>',\n ' </Box>',\n ' </DashboardProvider>',\n ' )',\n '}',\n ''\n ].join('\\n')\n}\n\n/**\n * Extensão das páginas do Pages Router pelo `pageExtensions` do `next.config` (ex.: `page.tsx` no\n * `custo`). Sem a opção (ou sem config), `tsx`.\n */\nexport function pageExtension(nextConfig: string | undefined) {\n return pageExtensions(nextConfig).find((extension) => extension.endsWith('tsx')) ?? 'tsx'\n}\n\n/** Itens do `pageExtensions` do `next.config` (lido como texto). */\nfunction pageExtensions(nextConfig: string | undefined) {\n const list = /pageExtensions\\s*:\\s*\\[([^\\]]*)\\]/.exec(nextConfig ?? '')?.[1]\n return [...(list ?? '').matchAll(/['\"]([^'\"]+)['\"]/g)].map((match) => match[1] as string)\n}\n\n// --- endpoint ---\n\nexport type ApiOptions = {\n /** Import do `models/index.ts` (o `dashboardModels`), relativo ao endpoint. */\n modelsImport: string\n /** Pastas do devtools, relativas à raiz do app (`process.cwd()` no servidor). */\n modelsDir: string\n layoutsDir: string\n snapshotsDir: string\n /** Import do registro de tipos de gráfico do servidor (`export const chartTypes`). */\n chartTypesImport?: string\n /** App Router do Next: `route.ts` com `POST(request)` em vez do handler do Pages Router. */\n appRouter?: boolean\n}\n\n/** Valores trocados num template de endpoint do app (`api.template`). */\nexport const apiTemplateValues = (options: ApiOptions): Record<string, string> => ({\n __MODELS__: options.modelsImport,\n __MODELS_DIR__: options.modelsDir,\n __LAYOUTS_DIR__: options.layoutsDir,\n __SNAPSHOTS_DIR__: options.snapshotsDir\n})\n\nexport function fillApiTemplate(template: string, options: ApiOptions) {\n let content = template\n for (const [marker, value] of Object.entries(apiTemplateValues(options)))\n content = content.replaceAll(marker, value)\n return content\n}\n\n/** `src/dashboard/models` → `'src', 'dashboard', 'models'` (argumentos do `path.join`). */\nconst pathArgs = (dir: string) =>\n dir\n .split(/[\\\\/]/)\n .filter(Boolean)\n .map((part) => literal(part))\n .join(', ')\n\n/**\n * Endpoint padrão do `runQuery` com Prisma: um só para todos os dashboards (só aceita os modelos do\n * `models/index.ts`), com o devtools ligado só em desenvolvimento.\n */\nexport function apiFileCode(options: ApiOptions) {\n const { modelsImport, chartTypesImport, appRouter } = options\n const dir = (path: string) => `path.join(process.cwd(), ${pathArgs(path)})`\n const run = (body: string, indent: string) =>\n [\n 'const result = await runQuery(',\n ` dashboardModels,`,\n ` ${body} as DashboardQuery,`,\n ' (sql, params) => prisma.$queryRawUnsafe<unknown[]>(sql, ...params),',\n ' { cacheTtlMs: CACHE_TTL_MS, devtools: DEVTOOLS }',\n ')'\n ].map((line) => `${indent}${line}`)\n return [\n \"import path from 'node:path'\",\n \"import { DashboardQueryError, runQuery } from '@bluemarble/bi/server'\",\n \"import type { DashboardQuery } from '@bluemarble/bi/types'\",\n \"import { PrismaClient } from '@prisma/client'\",\n ...(appRouter ? [] : [\"import type { NextApiRequest, NextApiResponse } from 'next'\"]),\n ...(chartTypesImport ? [`import { chartTypes } from '${chartTypesImport}'`] : []),\n `import { dashboardModels } from '${modelsImport}'`,\n '',\n '// Client próprio: rotas que chamam `prisma.$disconnect()` no client global derrubariam',\n '// consultas longas em andamento (\"Engine is not yet connected\").',\n 'const globalForPrisma = globalThis as unknown as { dashboardPrisma?: PrismaClient }',\n 'const prisma = globalForPrisma.dashboardPrisma ?? new PrismaClient()',\n \"if (process.env.NODE_ENV === 'development') globalForPrisma.dashboardPrisma = prisma\",\n '',\n 'const CACHE_TTL_MS = 5 * 60 * 1000',\n '',\n '// Devtools (debug, rascunho de modelo, salvar modelo/layout) aceita SQL vindo do browser: só em dev',\n \"const DEVTOOLS = process.env.NODE_ENV === 'development' && {\",\n ` modelsDir: ${dir(options.modelsDir)},`,\n ` snapshotsDir: ${dir(options.snapshotsDir)},`,\n ` layoutsDir: ${dir(options.layoutsDir)}${chartTypesImport ? ',' : ''}`,\n ...(chartTypesImport ? [' chartTypes'] : []),\n '}',\n '',\n '/**',\n ' * Endpoint genérico do `@bluemarble/bi`: recebe `{ model, dimensions, measures, filters }` e',\n ' * devolve as linhas agregadas. Só aceita nomes definidos nos modelos registrados.',\n ' */',\n ...(appRouter\n ? [\n 'export async function POST(request: Request) {',\n ' try {',\n ...run('(await request.json())', ' '),\n ' return Response.json(result)',\n ' } catch (error) {',\n ' if (error instanceof DashboardQueryError)',\n ' return Response.json({ error: error.message }, { status: 400 })',\n ' throw error',\n ' }',\n '}'\n ]\n : [\n 'export default async function handler(req: NextApiRequest, res: NextApiResponse) {',\n \" if (req.method !== 'POST') return res.status(405).setHeader('Allow', 'POST').end()\",\n ' try {',\n ...run('req.body', ' '),\n ' return res.json(result)',\n ' } catch (error) {',\n ' if (error instanceof DashboardQueryError)',\n ' return res.status(400).json({ error: error.message })',\n ' throw error',\n ' }',\n '}'\n ]),\n ''\n ].join('\\n')\n}\n\n/**\n * Extensão das rotas de API do Pages Router pelo `pageExtensions` do `next.config` (ex.: `api.ts`\n * no `custo`). Sem a opção (ou sem config), `ts`.\n */\nexport function apiExtension(nextConfig: string | undefined) {\n return pageExtensions(nextConfig).find((extension) => extension.endsWith('ts')) ?? 'ts'\n}\n","#!/usr/bin/env node\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { basename, dirname, join, relative, sep } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport {\n type ApiOptions,\n apiExtension,\n apiFileCode,\n checkLayoutId,\n checkModelName,\n fillApiTemplate,\n fillPageTemplate,\n kebabCase,\n layoutFileCode,\n modelFileCode,\n modelIndexCode,\n type PageOptions,\n pageExtension,\n pageFileCode,\n registerModel,\n ScaffoldError,\n titleCase\n} from './scaffold'\n\nconst HELP = `Uso: bm-bi new <nome> [opções]\n\nCria um dashboard vazio: o modelo (models/<nome>.ts, registrado em models/index.ts), o layout\n(layouts/<id>.json), a página e, se ainda não existir, o endpoint do runQuery (um só para todos os\ndashboards). Depois é montar tudo pelas abas Modelo e Layout do devtools.\n\nA página vai para o router do Next que existir: src/pages (ou pages) →\ndashboard/<id-em-kebab>/index.<ext>, com a extensão do pageExtensions do next.config; src/app (ou\napp) → dashboard/<id-em-kebab>/page.tsx, com 'use client'. Com <dir>/page.template, a página sai\ndele, trocando __MODEL__, __LAYOUT__ (import do JSON), __TITLE__, __COMPONENT__ e __ENDPOINT__.\n\nO endpoint vai para a rota do --endpoint no mesmo router: src/pages → api/dashboard/query.<ext> (ext\nde API do pageExtensions); src/app → api/dashboard/query/route.ts. O padrão usa Prisma\n($queryRawUnsafe); com <dir>/api.template, sai dele, trocando __MODELS__ (import do models/index),\n__MODELS_DIR__, __LAYOUTS_DIR__ e __SNAPSHOTS_DIR__ (pastas relativas à raiz do app).\n\nOpções:\n --id <id> id do layout (padrão: o nome do modelo)\n --title <texto> título da página (padrão: o id em palavras)\n --dir <pasta> pasta dos dashboards (padrão: src/dashboard)\n --models <pasta> pasta dos modelos (padrão: <dir>/models)\n --layouts <pasta> pasta dos layouts (padrão: <dir>/layouts)\n --page <arquivo> arquivo da página (padrão: pelo router do Next, ver acima)\n --skip-page não cria a página\n --template <arq.> template da página (padrão: <dir>/page.template, se existir)\n --endpoint <url> endpoint do runQuery (padrão: /api/dashboard/query)\n --api <arquivo> arquivo do endpoint (padrão: pela rota do --endpoint, ver acima)\n --skip-api não cria o endpoint\n --api-template <a.> template do endpoint (padrão: <dir>/api.template, se existir)\n --snapshots <pasta> pasta dos snapshots no endpoint (padrão: tests/dashboard/snapshots)\n -h, --help esta ajuda\n`\n\nconst exists = (path: string) => existsSync(path)\n\n/** Caminho de import de `to` a partir do arquivo `from` (sempre com `./` ou `../` e `/`). */\nfunction importPath(from: string, to: string) {\n const path = relative(dirname(from), to).split(sep).join('/')\n return path.startsWith('.') ? path : `./${path}`\n}\n\n/** Router do Next que existir no app (Pages primeiro, como o `custo`). */\nfunction nextRouter(cwd: string) {\n for (const pages of ['src/pages', 'pages']) {\n if (!exists(join(cwd, pages))) continue\n const config = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs']\n .map((file) => join(cwd, file))\n .find(exists)\n return { app: false, dir: join(cwd, pages), config: config && readFileSync(config, 'utf8') }\n }\n for (const app of ['src/app', 'app'])\n if (exists(join(cwd, app))) return { app: true, dir: join(cwd, app), config: undefined }\n return undefined\n}\n\ntype Router = ReturnType<typeof nextRouter>\n\nfunction defaultPagePath(router: Router, slug: string) {\n if (!router) return undefined\n return router.app\n ? join(router.dir, 'dashboard', slug, 'page.tsx')\n : join(router.dir, 'dashboard', slug, `index.${pageExtension(router.config)}`)\n}\n\n/** Endpoint na rota do `--endpoint` (ex.: `/api/dashboard/query`). */\nfunction defaultApiPath(router: Router, endpoint: string) {\n if (!router) return undefined\n if (!/^(\\/[\\w-]+)+$/.test(endpoint))\n throw new ScaffoldError(\n `Endpoint inválido para criar a rota: ${endpoint} (use --api ou --skip-api)`\n )\n const route = endpoint.slice(1).split('/')\n return router.app\n ? join(router.dir, ...route, 'route.ts')\n : join(router.dir, ...route.slice(0, -1), `${route.at(-1)}.${apiExtension(router.config)}`)\n}\n\n/** `export const charts` em <dir>/charts: os tipos de gráfico do app entram no provider. */\nfunction chartsIndex(dir: string) {\n return ['index.ts', 'index.tsx']\n .map((file) => join(dir, 'charts', file))\n .find((path) => exists(path) && /export\\s+const\\s+charts\\b/.test(readFileSync(path, 'utf8')))\n}\n\n/** `export const chartTypes` em <dir>/charts/types.ts: o devtools do endpoint valida o layout. */\nfunction chartTypesFile(dir: string) {\n return ['types.ts', 'types.tsx']\n .map((file) => join(dir, 'charts', file))\n .find(\n (path) => exists(path) && /export\\s+const\\s+chartTypes\\b/.test(readFileSync(path, 'utf8'))\n )\n}\n\n/** Caminho relativo à raiz do app, sempre com `/`. */\nconst fromRoot = (cwd: string, path: string) => relative(cwd, path).split(sep).join('/')\n\nfunction main(argv: string[]) {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n id: { type: 'string' },\n dir: { type: 'string', default: 'src/dashboard' },\n models: { type: 'string' },\n layouts: { type: 'string' },\n title: { type: 'string' },\n page: { type: 'string' },\n 'skip-page': { type: 'boolean' },\n template: { type: 'string' },\n endpoint: { type: 'string', default: '/api/dashboard/query' },\n api: { type: 'string' },\n 'skip-api': { type: 'boolean' },\n 'api-template': { type: 'string' },\n snapshots: { type: 'string', default: 'tests/dashboard/snapshots' },\n help: { type: 'boolean', short: 'h' }\n }\n })\n const [command, name, ...rest] = positionals\n if (values.help || !command) {\n process.stdout.write(HELP)\n return\n }\n if (command !== 'new') throw new ScaffoldError(`Comando desconhecido: ${command}\\n\\n${HELP}`)\n if (!name) throw new ScaffoldError(`Falta o nome do modelo.\\n\\n${HELP}`)\n if (rest.length) throw new ScaffoldError(`Argumentos a mais: ${rest.join(' ')}`)\n\n const id = values.id ?? name\n checkModelName(name)\n checkLayoutId(id)\n\n const cwd = process.cwd()\n const modelsDir = join(cwd, values.models ?? join(values.dir, 'models'))\n const layoutsDir = join(cwd, values.layouts ?? join(values.dir, 'layouts'))\n const modelPath = join(modelsDir, `${name}.ts`)\n const indexPath = join(modelsDir, 'index.ts')\n const layoutPath = join(layoutsDir, `${id}.json`)\n const dashboardDir = join(cwd, values.dir)\n const router = nextRouter(cwd)\n const pagePath = values['skip-page']\n ? undefined\n : values.page\n ? join(cwd, values.page)\n : defaultPagePath(router, kebabCase(id))\n const apiPath = values['skip-api']\n ? undefined\n : values.api\n ? join(cwd, values.api)\n : defaultApiPath(router, values.endpoint)\n // O endpoint é um só para todos os dashboards: se já existe, fica como está\n const apiExists = !!apiPath && exists(apiPath)\n const templatePath = values.template\n ? join(cwd, values.template)\n : [join(dashboardDir, 'page.template')].find(exists)\n const apiTemplatePath = values['api-template']\n ? join(cwd, values['api-template'])\n : [join(dashboardDir, 'api.template')].find(exists)\n for (const path of [templatePath, apiTemplatePath])\n if (path && !exists(path))\n throw new ScaffoldError(`Template não encontrado: ${relative(cwd, path)}`)\n\n // Confere tudo antes de escrever: ou cria o dashboard inteiro, ou nada\n for (const path of [modelPath, layoutPath, pagePath])\n if (path && existsSync(path)) throw new ScaffoldError(`Já existe: ${relative(cwd, path)}`)\n const index = existsSync(indexPath)\n ? registerModel(readFileSync(indexPath, 'utf8'), name)\n : modelIndexCode(name)\n let page: string | undefined\n if (pagePath) {\n const charts = chartsIndex(dashboardDir)\n const options: PageOptions = {\n model: name,\n layoutImport: importPath(pagePath, layoutPath),\n title: values.title ?? titleCase(id),\n endpoint: values.endpoint,\n // App Router: a página usa hooks, então é client component\n clientDirective:\n basename(pagePath).startsWith('page.') &&\n relative(cwd, pagePath).split(sep).includes('app'),\n chartsImport: charts && importPath(pagePath, dirname(charts))\n }\n page = templatePath\n ? fillPageTemplate(readFileSync(templatePath, 'utf8'), options)\n : pageFileCode(options)\n }\n let api: string | undefined\n if (apiPath && !apiExists) {\n const chartTypes = chartTypesFile(dashboardDir)\n const options: ApiOptions = {\n modelsImport: importPath(apiPath, modelsDir),\n modelsDir: fromRoot(cwd, modelsDir),\n layoutsDir: fromRoot(cwd, layoutsDir),\n snapshotsDir: fromRoot(cwd, join(cwd, values.snapshots)),\n chartTypesImport: chartTypes && importPath(apiPath, chartTypes.replace(/\\.tsx?$/, '')),\n appRouter: basename(apiPath).startsWith('route.')\n }\n api = apiTemplatePath\n ? fillApiTemplate(readFileSync(apiTemplatePath, 'utf8'), options)\n : apiFileCode(options)\n }\n\n const written: string[] = []\n const write = (path: string, content: string) => {\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, content)\n written.push(relative(cwd, path))\n }\n write(modelPath, modelFileCode(name))\n if (index !== null) write(indexPath, index)\n write(layoutPath, layoutFileCode(id))\n if (pagePath && page !== undefined) write(pagePath, page)\n if (apiPath && api !== undefined) write(apiPath, api)\n\n process.stdout.write(\n [\n ...written.map((path) => `✓ ${path}`),\n ...(pagePath\n ? templatePath\n ? [` (página do template ${relative(cwd, templatePath)})`]\n : []\n : values['skip-page']\n ? []\n : [\n '',\n 'Página não criada: não achei src/pages nem src/app. Use --page <arquivo>, com o layout:',\n ` <DashboardProvider model=\"${name}\" fetcher={fetcher}>`,\n ' <DashboardFilters layout={layout} />',\n ' <DashboardCharts layout={layout} />',\n ' </DashboardProvider>'\n ]),\n ...(apiPath\n ? apiExists\n ? [`· ${relative(cwd, apiPath)} (endpoint já existe, mantido)`]\n : apiTemplatePath\n ? [` (endpoint do template ${relative(cwd, apiTemplatePath)})`]\n : []\n : values['skip-api']\n ? []\n : ['', 'Endpoint não criado: não achei src/pages nem src/app. Use --api <arquivo>.']),\n ''\n ].join('\\n')\n )\n}\n\ntry {\n main(process.argv.slice(2))\n} catch (error) {\n if (!(error instanceof ScaffoldError) && !(error instanceof TypeError)) throw error\n process.stderr.write(`${error.message}\\n`)\n process.exitCode = 1\n}\n"],"mappings":"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"}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $ as ModelDef, C as BarValuePosition, E as CardStyle, L as DonutLegendPosition, M as ChartTypes, N as ChartsLayout, O as ChartDef, R as DonutValueContent, a as ValueRule, d as DashboardQueryDebug, et as ModelMeta, f as DashboardResult, g as FilterValue, i as FormatRule, j as ChartTypeSpec, k as ChartNumberFormat, l as DashboardFilters$1, n as ColumnAlign, p as DashboardRow, r as ColumnVerticalAlign, t as ColorScale, u as DashboardQuery } from "../formatRules-DxA9vMvz.js";
|
|
2
2
|
import { ComponentType, MouseEvent, ReactNode } from "react";
|
|
3
3
|
//#region src/client/BarChart.d.ts
|
|
4
4
|
type BarChartProps = {
|
|
@@ -44,6 +44,13 @@ type BarChartProps = {
|
|
|
44
44
|
valueSize?: number;
|
|
45
45
|
/** Empilhado: total da pilha na ponta da barra. */
|
|
46
46
|
showTotal?: boolean;
|
|
47
|
+
/** Só mostra o valor (e o total) que atende à regra, ex.: `{ operator: '!=', value: 0 }`. */
|
|
48
|
+
valueRule?: ValueRule;
|
|
49
|
+
/**
|
|
50
|
+
* Com `clickToFilter`: o filtro da dimensão (e da série) esconde o resto em vez de só apagá-lo
|
|
51
|
+
* (a consulta passa a respeitar o próprio filtro). Padrão: `false` (apaga).
|
|
52
|
+
*/
|
|
53
|
+
hideUnselected?: boolean;
|
|
47
54
|
/**
|
|
48
55
|
* Cor por série: `{ medida: '#rrggbb' }` ou, com `series`, `{ valor: '#rrggbb' }` (também
|
|
49
56
|
* "(em branco)" e "Outros"). O que faltar segue a paleta.
|
|
@@ -64,7 +71,7 @@ type BarChartProps = {
|
|
|
64
71
|
* Com `series`, uma medida quebrada pelos valores de outra dimensão:
|
|
65
72
|
* `<BarChart dimension="mes" series="tipo_descarga" measures={['volume']} stacked />`.
|
|
66
73
|
*/
|
|
67
|
-
declare function BarChart({ dimension, measures: measureList, series, formatSeries, maxSeries, orientation, stacked, sort, limit, formatDimension, hideEmpty, showEmpty, showValues, valuePosition, valueColor, valueSize, showTotal, colors, clickToFilter, height, labels, formats: formatsByMeasure, queryLabel }: BarChartProps): import("react").JSX.Element;
|
|
74
|
+
declare function BarChart({ dimension, measures: measureList, series, formatSeries, maxSeries, orientation, stacked, sort, limit, formatDimension, hideEmpty, showEmpty, showValues, valuePosition, valueColor, valueSize, showTotal, valueRule, hideUnselected, colors, clickToFilter, height, labels, formats: formatsByMeasure, queryLabel }: BarChartProps): import("react").JSX.Element;
|
|
68
75
|
//#endregion
|
|
69
76
|
//#region src/client/ComboChart.d.ts
|
|
70
77
|
type ComboChartProps = {
|
|
@@ -199,9 +206,30 @@ type DashboardChartsProps = {
|
|
|
199
206
|
formatters?: Record<string, (value: FilterValue) => string>;
|
|
200
207
|
/** Moldura de cada gráfico (título + conteúdo). */
|
|
201
208
|
panel?: ComponentType<ChartPanelProps>;
|
|
202
|
-
/** Espaço entre os cards (unidades do `theme.spacing`);
|
|
209
|
+
/** Espaço entre os cards (unidades do `theme.spacing`); `cards.rowGap`/`columnGap` do layout (px) têm prioridade. */
|
|
203
210
|
gap?: number;
|
|
204
211
|
};
|
|
212
|
+
/**
|
|
213
|
+
* `sx` da moldura de um card com o estilo do layout (`card` do `panel`). Só traz o que o layout
|
|
214
|
+
* definiu, para um `panel` do app espalhar por cima do visual dele:
|
|
215
|
+
* `<Paper sx={{ p: 2, borderRadius: 0, ...chartCardSx(card) }}>`.
|
|
216
|
+
*/
|
|
217
|
+
declare function chartCardSx(card: CardStyle): {
|
|
218
|
+
boxSizing: 'border-box';
|
|
219
|
+
p?: string | undefined;
|
|
220
|
+
bgcolor?: string | undefined;
|
|
221
|
+
borderWidth?: string | undefined;
|
|
222
|
+
borderStyle?: string | undefined;
|
|
223
|
+
borderColor?: string | undefined;
|
|
224
|
+
borderRadius?: string | undefined;
|
|
225
|
+
boxShadow?: number | undefined;
|
|
226
|
+
};
|
|
227
|
+
/** `sx` do título de um card com o estilo do layout (idem `chartCardSx`). */
|
|
228
|
+
declare function chartTitleSx(card: CardStyle): Partial<{
|
|
229
|
+
color: string | undefined;
|
|
230
|
+
fontSize: string | undefined;
|
|
231
|
+
textAlign: "center" | "left" | "right" | undefined;
|
|
232
|
+
}>;
|
|
205
233
|
/**
|
|
206
234
|
* Renderiza os gráficos de um layout declarativo num grid de 12 colunas (no celular, um por linha;
|
|
207
235
|
* dentro de um `<DashboardCanvas>` de tamanho fixo, sempre as 12). Em desenvolvimento, a aba
|
|
@@ -437,6 +465,11 @@ type DataTableProps = {
|
|
|
437
465
|
* uma dimensão/medida (da tabela, de fora dela ou `%:medida`).
|
|
438
466
|
*/
|
|
439
467
|
formatRules?: FormatRule[];
|
|
468
|
+
/**
|
|
469
|
+
* Escalas de cor (gradiente): o fundo ou o texto de uma coluna numérica vai de `minColor` a
|
|
470
|
+
* `color` conforme o valor (ex.: 0 a 100). As regras de `formatRules` ficam por cima.
|
|
471
|
+
*/
|
|
472
|
+
colorScales?: ColorScale[];
|
|
440
473
|
/** Bordas entre as células (grade). Padrão: só a linha embaixo de cada linha. */
|
|
441
474
|
cellBorders?: boolean;
|
|
442
475
|
/** Cor das bordas da grade. Padrão: `#e0e0e0`. */
|
|
@@ -455,7 +488,7 @@ type DataTableProps = {
|
|
|
455
488
|
* Tabela de medidas por dimensões, com total e ordenação pelo cabeçalho. Ex.:
|
|
456
489
|
* `<DataTable dimensions={['navio']} measures={['volTotal', 'custoTotal', 'custoTon']} />`.
|
|
457
490
|
*/
|
|
458
|
-
declare function DataTable({ dimensions, measures, percentOf, formatDimensions, showTotal, sortBy, sortDirection, clickToFilter, height, labels, formats: formatsByMeasure, percentDecimals, columnOrder, formatRules, cellBorders, borderColor, align, verticalAlign, queryLabel }: DataTableProps): import("react").JSX.Element;
|
|
491
|
+
declare function DataTable({ dimensions, measures, percentOf, formatDimensions, showTotal, sortBy, sortDirection, clickToFilter, height, labels, formats: formatsByMeasure, percentDecimals, columnOrder, formatRules, colorScales, cellBorders, borderColor, align, verticalAlign, queryLabel }: DataTableProps): import("react").JSX.Element;
|
|
459
492
|
//#endregion
|
|
460
493
|
//#region src/client/DonutChart.d.ts
|
|
461
494
|
type DonutChartProps = {
|
|
@@ -474,14 +507,23 @@ type DonutChartProps = {
|
|
|
474
507
|
formats?: Record<string, ChartNumberFormat>;
|
|
475
508
|
/** Casas decimais do percentual. Padrão: 1. */
|
|
476
509
|
percentDecimals?: number;
|
|
510
|
+
/** Lado da legenda. Padrão: `right` (nome, % e valor; em cima/embaixo, só o nome, em linha). */
|
|
511
|
+
legendPosition?: DonutLegendPosition;
|
|
512
|
+
/** Valor de cada fatia do lado de fora da rosca. */
|
|
513
|
+
showValues?: boolean;
|
|
514
|
+
/** O que o valor de fora mostra. Padrão: `both` (`44.981 (51%)`). */
|
|
515
|
+
valueContent?: DonutValueContent;
|
|
516
|
+
/** Total no furo da rosca. Padrão: `true`. */
|
|
517
|
+
showTotal?: boolean;
|
|
477
518
|
/** Nome da consulta no devtools. */
|
|
478
519
|
queryLabel?: string;
|
|
479
520
|
};
|
|
480
521
|
/**
|
|
481
|
-
* Rosca de uma medida por uma dimensão, com o total no centro e a legenda ao lado (nome, % e valor)
|
|
522
|
+
* Rosca de uma medida por uma dimensão, com o total no centro e a legenda ao lado (nome, % e valor)
|
|
523
|
+
* ou em cima/embaixo; `showValues` põe o valor de cada fatia do lado de fora.
|
|
482
524
|
* Ex.: `<DonutChart dimension="operacao_status" measure="custoTotal" />`.
|
|
483
525
|
*/
|
|
484
|
-
declare function DonutChart({ dimension, measure, maxSlices, formatDimension, clickToFilter, height, labels, formats, percentDecimals, queryLabel }: DonutChartProps): import("react").JSX.Element;
|
|
526
|
+
declare function DonutChart({ dimension, measure, maxSlices, formatDimension, clickToFilter, height, labels, formats, percentDecimals, legendPosition, showValues, valueContent, showTotal, queryLabel }: DonutChartProps): import("react").JSX.Element;
|
|
485
527
|
//#endregion
|
|
486
528
|
//#region src/client/FilterSelect.d.ts
|
|
487
529
|
type FilterSelectProps = {
|
|
@@ -575,5 +617,5 @@ declare function useModelQuery({ dimensions, measures, ignoreFilters, label, ski
|
|
|
575
617
|
};
|
|
576
618
|
};
|
|
577
619
|
//#endregion
|
|
578
|
-
export { BarChart, type ChartComponentProps, type ChartComponentSpec, type ChartPanelProps, ComboChart, DashboardCanvas, DashboardCharts, type DashboardFetcher, DashboardFilters, DashboardProvider, DataTable, DonutChart, FilterSelect, INSPECT_ATTRIBUTE, KpiCard, Legend, ModelDictionary, OTHER_COLOR, SERIES_COLORS, TooltipBox, chartPlaceholder, fieldLabel, formatCurrency, formatMeasure, formatNumber, measureFormat, measureFormatter, numberOf, seriesColor, useClickFilter, useDashboard, useModelQuery };
|
|
620
|
+
export { BarChart, type ChartComponentProps, type ChartComponentSpec, type ChartPanelProps, ComboChart, DashboardCanvas, DashboardCharts, type DashboardFetcher, DashboardFilters, DashboardProvider, DataTable, DonutChart, FilterSelect, INSPECT_ATTRIBUTE, KpiCard, Legend, ModelDictionary, OTHER_COLOR, SERIES_COLORS, TooltipBox, chartCardSx, chartPlaceholder, chartTitleSx, fieldLabel, formatCurrency, formatMeasure, formatNumber, measureFormat, measureFormatter, numberOf, seriesColor, useClickFilter, useDashboard, useModelQuery };
|
|
579
621
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/client/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { S as layoutItemKey, _ as useClickFilter, a as formatCurrency, c as measureFormat, d as OTHER_COLOR, f as SERIES_COLORS, g as seriesColor, h as numberOf, i as fieldLabel, l as measureFormatter, m as chartPlaceholder, n as INSPECT_ATTRIBUTE, o as formatMeasure, p as TooltipBox, r as useModelQuery, s as formatNumber, t as ModelDictionary, u as Legend, v as DashboardProvider, x as LAYOUT_ITEM_ATTRIBUTE, y as useDashboard } from "../ModelDictionary-
|
|
1
|
+
import { A as contrastText, M as rowRuleStyles, d as customChartProps, g as percentColumn, j as matchesValueRule, k as colorScaleStyles, o as cardsGap, p as isBuiltinChart, s as chartCardStyle, v as tableColumns } from "../layout-BS3nO6OT.js";
|
|
2
|
+
import { S as layoutItemKey, _ as useClickFilter, a as formatCurrency, c as measureFormat, d as OTHER_COLOR, f as SERIES_COLORS, g as seriesColor, h as numberOf, i as fieldLabel, l as measureFormatter, m as chartPlaceholder, n as INSPECT_ATTRIBUTE, o as formatMeasure, p as TooltipBox, r as useModelQuery, s as formatNumber, t as ModelDictionary, u as Legend, v as DashboardProvider, x as LAYOUT_ITEM_ATTRIBUTE, y as useDashboard } from "../ModelDictionary-Cw4sa8tW.js";
|
|
3
3
|
import { Box, Checkbox, CircularProgress, FormControl, InputLabel, ListItemText, MenuItem, OutlinedInput, Paper, Select, Table, TableBody, TableCell, TableHead, TableRow, TableSortLabel, Typography } from "@mui/material";
|
|
4
4
|
import { Bar, BarChart as BarChart$1, CartesianGrid, Cell, ComposedChart, LabelList, Line, Pie, PieChart, ResponsiveContainer, Tooltip as Tooltip$1, XAxis, YAxis } from "recharts";
|
|
5
5
|
import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
@@ -134,13 +134,14 @@ const ROW_HEIGHT = 28;
|
|
|
134
134
|
* Com `series`, uma medida quebrada pelos valores de outra dimensão:
|
|
135
135
|
* `<BarChart dimension="mes" series="tipo_descarga" measures={['volume']} stacked />`.
|
|
136
136
|
*/
|
|
137
|
-
function BarChart({ dimension, measures: measureList, series, formatSeries = String, maxSeries = 6, orientation = "vertical", stacked = false, sort = "dimension", limit, formatDimension = String, hideEmpty = true, showEmpty = false, showValues = true, valuePosition = "outside", valueColor, valueSize = 12, showTotal = false, colors, clickToFilter = false, height = 360, labels, formats: formatsByMeasure, queryLabel }) {
|
|
137
|
+
function BarChart({ dimension, measures: measureList, series, formatSeries = String, maxSeries = 6, orientation = "vertical", stacked = false, sort = "dimension", limit, formatDimension = String, hideEmpty = true, showEmpty = false, showValues = true, valuePosition = "outside", valueColor, valueSize = 12, showTotal = false, valueRule, hideUnselected = false, colors, clickToFilter = false, height = 360, labels, formats: formatsByMeasure, queryLabel }) {
|
|
138
138
|
const { meta, filters, setFilter } = useDashboard();
|
|
139
|
+
const dimming = clickToFilter && !hideUnselected;
|
|
139
140
|
const measures = series ? measureList.slice(0, 1) : measureList;
|
|
140
141
|
const { rows, isLoading, error, inspectProps } = useModelQuery({
|
|
141
142
|
dimensions: series ? [dimension, series] : [dimension],
|
|
142
143
|
measures,
|
|
143
|
-
ignoreFilters:
|
|
144
|
+
ignoreFilters: dimming ? series ? [dimension, series] : [dimension] : void 0,
|
|
144
145
|
label: queryLabel ?? `BarChart · ${dimension}`
|
|
145
146
|
});
|
|
146
147
|
const domain = useModelQuery({
|
|
@@ -153,7 +154,7 @@ function BarChart({ dimension, measures: measureList, series, formatSeries = Str
|
|
|
153
154
|
const table = meta?.dimensions[dimension]?.table;
|
|
154
155
|
const categories = useModelQuery({
|
|
155
156
|
dimensions: [dimension],
|
|
156
|
-
ignoreFilters: Object.keys(filters).filter((name) =>
|
|
157
|
+
ignoreFilters: Object.keys(filters).filter((name) => dimming && name === dimension || !table || meta?.dimensions[name]?.table !== table),
|
|
157
158
|
label: `${queryLabel ?? `BarChart · ${dimension}`} · categorias`,
|
|
158
159
|
skip: !showEmpty || !meta
|
|
159
160
|
});
|
|
@@ -420,13 +421,13 @@ function BarChart({ dimension, measures: measureList, series, formatSeries = Str
|
|
|
420
421
|
segmentLabels && /* @__PURE__ */ jsx(LabelList, {
|
|
421
422
|
dataKey: bar.dataKey,
|
|
422
423
|
position: position[valuePosition],
|
|
423
|
-
formatter: (value) => stacked && !Number(value) ? "" : bar.format(value),
|
|
424
|
+
formatter: (value) => stacked && !Number(value) || !matchesValueRule(valueRule, Number(value)) ? "" : bar.format(value),
|
|
424
425
|
style: labelStyle(valuePosition === "outside")
|
|
425
426
|
}),
|
|
426
427
|
totalLabel && index === bars.length - 1 && /* @__PURE__ */ jsx(LabelList, {
|
|
427
428
|
dataKey: TOTAL_KEY,
|
|
428
429
|
position: position.outside,
|
|
429
|
-
formatter: (value) => (bars[0]?.format ?? String)(value),
|
|
430
|
+
formatter: (value) => matchesValueRule(valueRule, Number(value)) ? (bars[0]?.format ?? String)(value) : "",
|
|
430
431
|
style: {
|
|
431
432
|
...labelStyle(true),
|
|
432
433
|
...valuePosition !== "outside" && { fill: "#333" },
|
|
@@ -818,6 +819,7 @@ function DashboardCanvas({ layout, children }) {
|
|
|
818
819
|
//#region src/client/DataTable.tsx
|
|
819
820
|
const EMPTY = [];
|
|
820
821
|
const NO_RULES = [];
|
|
822
|
+
const NO_SCALES = [];
|
|
821
823
|
function compare(a, b) {
|
|
822
824
|
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
823
825
|
return String(a).localeCompare(String(b), "pt-BR", { numeric: true });
|
|
@@ -826,7 +828,7 @@ function compare(a, b) {
|
|
|
826
828
|
* Tabela de medidas por dimensões, com total e ordenação pelo cabeçalho. Ex.:
|
|
827
829
|
* `<DataTable dimensions={['navio']} measures={['volTotal', 'custoTotal', 'custoTon']} />`.
|
|
828
830
|
*/
|
|
829
|
-
function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDimensions = {}, showTotal = true, sortBy, sortDirection = "desc", clickToFilter = false, height = 400, labels, formats: formatsByMeasure, percentDecimals = 1, columnOrder, formatRules = NO_RULES, cellBorders = false, borderColor = "#e0e0e0", align, verticalAlign, queryLabel }) {
|
|
831
|
+
function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDimensions = {}, showTotal = true, sortBy, sortDirection = "desc", clickToFilter = false, height = 400, labels, formats: formatsByMeasure, percentDecimals = 1, columnOrder, formatRules = NO_RULES, colorScales = NO_SCALES, cellBorders = false, borderColor = "#e0e0e0", align, verticalAlign, queryLabel }) {
|
|
830
832
|
const { meta } = useDashboard();
|
|
831
833
|
const name = queryLabel ?? `DataTable · ${dimensions.join(", ") || "total"}`;
|
|
832
834
|
const firstDimension = dimensions[0];
|
|
@@ -888,11 +890,31 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
|
|
|
888
890
|
return value === null || value === void 0 || !total ? null : Number(value) / total;
|
|
889
891
|
};
|
|
890
892
|
const columns = tableColumns([...dimensions, ...measures], percentOf.filter((measure) => !dimensions.includes(measure)), columnOrder);
|
|
891
|
-
const
|
|
892
|
-
if (!
|
|
893
|
+
const evaluate = (row) => {
|
|
894
|
+
if (!percentOf.length) return row;
|
|
893
895
|
const evaluated = { ...row };
|
|
894
896
|
for (const measure of percentOf) evaluated[percentColumn(measure)] = percentOfRow(row, measure);
|
|
895
|
-
return
|
|
897
|
+
return evaluated;
|
|
898
|
+
};
|
|
899
|
+
const scaleStyles = colorScales.length ? colorScaleStyles(colorScales, rows.map(evaluate)) : null;
|
|
900
|
+
const ruleStyles = (row) => {
|
|
901
|
+
if (!formatRules.length && !scaleStyles) return void 0;
|
|
902
|
+
const evaluated = evaluate(row);
|
|
903
|
+
const rules = formatRules.length ? rowRuleStyles(formatRules, evaluated, columns) : {};
|
|
904
|
+
if (!scaleStyles) return rules;
|
|
905
|
+
const scaled = scaleStyles(evaluated);
|
|
906
|
+
return Object.fromEntries(columns.map((column) => {
|
|
907
|
+
const [scale, rule] = [scaled[column], rules[column]];
|
|
908
|
+
const style = {
|
|
909
|
+
...scale,
|
|
910
|
+
...rule
|
|
911
|
+
};
|
|
912
|
+
if (scale?.background && !rule?.background && !style.color) {
|
|
913
|
+
const text = contrastText(scale.background);
|
|
914
|
+
if (text) style.color = text;
|
|
915
|
+
}
|
|
916
|
+
return [column, style];
|
|
917
|
+
}));
|
|
896
918
|
};
|
|
897
919
|
const ruleSx = (style) => style && {
|
|
898
920
|
...style.color && { color: style.color },
|
|
@@ -1021,11 +1043,22 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
|
|
|
1021
1043
|
//#endregion
|
|
1022
1044
|
//#region src/client/DonutChart.tsx
|
|
1023
1045
|
const OTHERS = "Outros";
|
|
1046
|
+
/** Fatia abaixo dessa fração fica sem valor de fora (os rótulos se atropelariam); tem o tooltip. */
|
|
1047
|
+
const MIN_VALUE_SHARE = .02;
|
|
1048
|
+
const RADIAN = Math.PI / 180;
|
|
1049
|
+
/** Direção da caixa (rosca + legenda) conforme o lado da legenda. */
|
|
1050
|
+
const LEGEND_DIRECTION = {
|
|
1051
|
+
right: "row",
|
|
1052
|
+
left: "row-reverse",
|
|
1053
|
+
top: "column-reverse",
|
|
1054
|
+
bottom: "column"
|
|
1055
|
+
};
|
|
1024
1056
|
/**
|
|
1025
|
-
* Rosca de uma medida por uma dimensão, com o total no centro e a legenda ao lado (nome, % e valor)
|
|
1057
|
+
* Rosca de uma medida por uma dimensão, com o total no centro e a legenda ao lado (nome, % e valor)
|
|
1058
|
+
* ou em cima/embaixo; `showValues` põe o valor de cada fatia do lado de fora.
|
|
1026
1059
|
* Ex.: `<DonutChart dimension="operacao_status" measure="custoTotal" />`.
|
|
1027
1060
|
*/
|
|
1028
|
-
function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = String, clickToFilter = false, height = 300, labels, formats, percentDecimals = 1, queryLabel }) {
|
|
1061
|
+
function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = String, clickToFilter = false, height = 300, labels, formats, percentDecimals = 1, legendPosition = "right", showValues = false, valueContent = "both", showTotal = true, queryLabel }) {
|
|
1029
1062
|
const { meta } = useDashboard();
|
|
1030
1063
|
const { rows, isLoading, error, inspectProps } = useModelQuery({
|
|
1031
1064
|
dimensions: [dimension],
|
|
@@ -1068,6 +1101,26 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
|
|
|
1068
1101
|
if (slice?.value !== null && slice?.value !== void 0) filter.toggle(slice.value, event);
|
|
1069
1102
|
};
|
|
1070
1103
|
const dimmed = (slice) => filter.selected.length > 0 && (slice.value === null || filter.isDimmed(slice.value));
|
|
1104
|
+
const valueText = (amount) => valueContent === "value" ? format(amount) : valueContent === "percent" ? percent(amount) : `${format(amount)} (${percent(amount)})`;
|
|
1105
|
+
const SliceValue = ({ cx, cy, midAngle, outerRadius, payload }) => {
|
|
1106
|
+
const slice = payload;
|
|
1107
|
+
if (slice.amount / total < MIN_VALUE_SHARE) return null;
|
|
1108
|
+
const radius = outerRadius + 10;
|
|
1109
|
+
const cos = Math.cos(-midAngle * RADIAN);
|
|
1110
|
+
const x = cx + radius * cos;
|
|
1111
|
+
const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
|
1112
|
+
return /* @__PURE__ */ jsx("text", {
|
|
1113
|
+
x,
|
|
1114
|
+
y,
|
|
1115
|
+
textAnchor: Math.abs(cos) < .15 ? "middle" : cos > 0 ? "start" : "end",
|
|
1116
|
+
dominantBaseline: "central",
|
|
1117
|
+
fontSize: 12,
|
|
1118
|
+
fill: "#333",
|
|
1119
|
+
opacity: dimmed(slice) ? .45 : 1,
|
|
1120
|
+
children: valueText(slice.amount)
|
|
1121
|
+
});
|
|
1122
|
+
};
|
|
1123
|
+
const side = legendPosition === "left" || legendPosition === "right";
|
|
1071
1124
|
const SliceTooltip = ({ active, payload }) => {
|
|
1072
1125
|
if (!active || !payload?.length) return null;
|
|
1073
1126
|
const slice = payload[0].payload;
|
|
@@ -1093,17 +1146,21 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
|
|
|
1093
1146
|
sx: {
|
|
1094
1147
|
height,
|
|
1095
1148
|
display: "flex",
|
|
1096
|
-
|
|
1097
|
-
|
|
1149
|
+
flexDirection: LEGEND_DIRECTION[legendPosition],
|
|
1150
|
+
alignItems: side ? "center" : "stretch",
|
|
1151
|
+
gap: side ? 2 : 1,
|
|
1098
1152
|
opacity: isLoading ? .5 : 1,
|
|
1099
1153
|
transition: "opacity .2s"
|
|
1100
1154
|
},
|
|
1101
1155
|
children: [/* @__PURE__ */ jsxs(Box, {
|
|
1102
1156
|
sx: {
|
|
1103
1157
|
position: "relative",
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1158
|
+
minWidth: 140,
|
|
1159
|
+
minHeight: 0,
|
|
1160
|
+
...side ? {
|
|
1161
|
+
height: "100%",
|
|
1162
|
+
flex: showValues ? "1 1 0" : "0 0 50%"
|
|
1163
|
+
} : { flex: "1 1 0" }
|
|
1107
1164
|
},
|
|
1108
1165
|
children: [/* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
1109
1166
|
width: "100%",
|
|
@@ -1112,13 +1169,15 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
|
|
|
1112
1169
|
data: slices,
|
|
1113
1170
|
dataKey: "amount",
|
|
1114
1171
|
nameKey: "label",
|
|
1115
|
-
innerRadius: "62%",
|
|
1116
|
-
outerRadius: "92%",
|
|
1172
|
+
innerRadius: showValues ? "46%" : "62%",
|
|
1173
|
+
outerRadius: showValues ? "68%" : "92%",
|
|
1117
1174
|
startAngle: 90,
|
|
1118
1175
|
endAngle: -270,
|
|
1119
1176
|
stroke: "#fff",
|
|
1120
1177
|
strokeWidth: 2,
|
|
1121
1178
|
isAnimationActive: false,
|
|
1179
|
+
label: showValues ? SliceValue : false,
|
|
1180
|
+
labelLine: false,
|
|
1122
1181
|
onClick: (_, index, event) => select(slices[index], event),
|
|
1123
1182
|
cursor: clickToFilter ? "pointer" : void 0,
|
|
1124
1183
|
children: slices.map((slice) => /* @__PURE__ */ jsx(Cell, {
|
|
@@ -1126,7 +1185,7 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
|
|
|
1126
1185
|
fillOpacity: dimmed(slice) ? .35 : 1
|
|
1127
1186
|
}, slice.key))
|
|
1128
1187
|
})] })
|
|
1129
|
-
}), /* @__PURE__ */ jsxs(Box, {
|
|
1188
|
+
}), showTotal && /* @__PURE__ */ jsxs(Box, {
|
|
1130
1189
|
sx: {
|
|
1131
1190
|
position: "absolute",
|
|
1132
1191
|
inset: 0,
|
|
@@ -1152,14 +1211,15 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
|
|
|
1152
1211
|
children: format(total)
|
|
1153
1212
|
})]
|
|
1154
1213
|
})]
|
|
1155
|
-
}), /* @__PURE__ */ jsx(Box, {
|
|
1214
|
+
}), side ? /* @__PURE__ */ jsx(Box, {
|
|
1156
1215
|
component: "ul",
|
|
1157
1216
|
sx: {
|
|
1158
1217
|
listStyle: "none",
|
|
1159
1218
|
m: 0,
|
|
1160
1219
|
p: 0,
|
|
1161
1220
|
minWidth: 0,
|
|
1162
|
-
flex: 1,
|
|
1221
|
+
flex: showValues ? "0 1 auto" : 1,
|
|
1222
|
+
maxWidth: showValues ? "45%" : void 0,
|
|
1163
1223
|
overflow: "auto",
|
|
1164
1224
|
maxHeight: "100%"
|
|
1165
1225
|
},
|
|
@@ -1210,6 +1270,43 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
|
|
|
1210
1270
|
})
|
|
1211
1271
|
]
|
|
1212
1272
|
}, slice.key))
|
|
1273
|
+
}) : /* @__PURE__ */ jsx(Box, {
|
|
1274
|
+
component: "ul",
|
|
1275
|
+
sx: {
|
|
1276
|
+
listStyle: "none",
|
|
1277
|
+
m: 0,
|
|
1278
|
+
p: 0,
|
|
1279
|
+
display: "flex",
|
|
1280
|
+
flexWrap: "wrap",
|
|
1281
|
+
justifyContent: "center",
|
|
1282
|
+
columnGap: 1.5,
|
|
1283
|
+
rowGap: .25,
|
|
1284
|
+
flex: "0 0 auto",
|
|
1285
|
+
maxHeight: "40%",
|
|
1286
|
+
overflow: "auto"
|
|
1287
|
+
},
|
|
1288
|
+
children: slices.map((slice) => /* @__PURE__ */ jsxs(Box, {
|
|
1289
|
+
component: "li",
|
|
1290
|
+
title: `${format(slice.amount)} · ${percent(slice.amount)}`,
|
|
1291
|
+
onClick: (event) => select(slice, event),
|
|
1292
|
+
sx: {
|
|
1293
|
+
display: "flex",
|
|
1294
|
+
alignItems: "center",
|
|
1295
|
+
gap: .5,
|
|
1296
|
+
fontSize: 13,
|
|
1297
|
+
color: "#333",
|
|
1298
|
+
whiteSpace: "nowrap",
|
|
1299
|
+
opacity: dimmed(slice) ? .45 : 1,
|
|
1300
|
+
cursor: clickToFilter && slice.value !== null ? "pointer" : void 0
|
|
1301
|
+
},
|
|
1302
|
+
children: [/* @__PURE__ */ jsx(Box, { sx: {
|
|
1303
|
+
width: 10,
|
|
1304
|
+
height: 10,
|
|
1305
|
+
borderRadius: "50%",
|
|
1306
|
+
bgcolor: slice.color,
|
|
1307
|
+
flex: "0 0 auto"
|
|
1308
|
+
} }), slice.label]
|
|
1309
|
+
}, slice.key))
|
|
1213
1310
|
})]
|
|
1214
1311
|
})
|
|
1215
1312
|
});
|
|
@@ -1295,26 +1392,52 @@ function KpiCard({ measure, label, secondary, secondaryLabel, labels, formats, q
|
|
|
1295
1392
|
}
|
|
1296
1393
|
//#endregion
|
|
1297
1394
|
//#region src/client/DashboardCharts.tsx
|
|
1395
|
+
const px = (value) => value === void 0 ? void 0 : `${value}px`;
|
|
1396
|
+
/** Tira as chaves `undefined`, para o estilo do layout só sobrepor o que foi definido. */
|
|
1397
|
+
const defined = (object) => Object.fromEntries(Object.entries(object).filter(([, value]) => value !== void 0));
|
|
1398
|
+
/**
|
|
1399
|
+
* `sx` da moldura de um card com o estilo do layout (`card` do `panel`). Só traz o que o layout
|
|
1400
|
+
* definiu, para um `panel` do app espalhar por cima do visual dele:
|
|
1401
|
+
* `<Paper sx={{ p: 2, borderRadius: 0, ...chartCardSx(card) }}>`.
|
|
1402
|
+
*/
|
|
1403
|
+
function chartCardSx(card) {
|
|
1404
|
+
return {
|
|
1405
|
+
boxSizing: "border-box",
|
|
1406
|
+
...defined({
|
|
1407
|
+
p: px(card.padding),
|
|
1408
|
+
bgcolor: card.background,
|
|
1409
|
+
borderWidth: px(card.borderWidth),
|
|
1410
|
+
borderStyle: card.borderWidth === void 0 ? void 0 : "solid",
|
|
1411
|
+
borderColor: card.borderColor,
|
|
1412
|
+
borderRadius: px(card.borderRadius),
|
|
1413
|
+
boxShadow: card.shadow
|
|
1414
|
+
})
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
/** `sx` do título de um card com o estilo do layout (idem `chartCardSx`). */
|
|
1418
|
+
function chartTitleSx(card) {
|
|
1419
|
+
return defined({
|
|
1420
|
+
color: card.titleColor,
|
|
1421
|
+
fontSize: px(card.titleSize),
|
|
1422
|
+
textAlign: card.titleAlign
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1298
1425
|
function DefaultPanel({ title, card, children }) {
|
|
1299
|
-
const px = (value) => value === void 0 ? void 0 : `${value}px`;
|
|
1300
1426
|
return /* @__PURE__ */ jsxs(Paper, {
|
|
1301
|
-
elevation:
|
|
1427
|
+
elevation: 0,
|
|
1302
1428
|
sx: {
|
|
1303
|
-
p:
|
|
1429
|
+
p: 1.5,
|
|
1304
1430
|
minWidth: 0,
|
|
1305
1431
|
height: "100%",
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
borderRadius: px(card.borderRadius)
|
|
1432
|
+
border: "1px solid",
|
|
1433
|
+
borderColor: "divider",
|
|
1434
|
+
...chartCardSx(card)
|
|
1310
1435
|
},
|
|
1311
1436
|
children: [title && /* @__PURE__ */ jsx(Typography, {
|
|
1312
1437
|
sx: {
|
|
1313
1438
|
fontWeight: 700,
|
|
1314
1439
|
mb: 1,
|
|
1315
|
-
|
|
1316
|
-
fontSize: px(card.titleSize),
|
|
1317
|
-
textAlign: card.titleAlign
|
|
1440
|
+
...chartTitleSx(card)
|
|
1318
1441
|
},
|
|
1319
1442
|
children: title
|
|
1320
1443
|
}), children]
|
|
@@ -1335,10 +1458,12 @@ function DashboardCharts({ layout, formatters = {}, panel: Panel = DefaultPanel,
|
|
|
1335
1458
|
const { charts } = current;
|
|
1336
1459
|
const fixed = useFixedCanvas();
|
|
1337
1460
|
const columns = `repeat(12, minmax(0, 1fr))`;
|
|
1461
|
+
const space = cardsGap(current.cards);
|
|
1338
1462
|
return /* @__PURE__ */ jsx(Box, {
|
|
1339
1463
|
sx: {
|
|
1340
1464
|
display: "grid",
|
|
1341
|
-
|
|
1465
|
+
rowGap: px(space.row) ?? gap,
|
|
1466
|
+
columnGap: px(space.column) ?? gap,
|
|
1342
1467
|
gridTemplateColumns: fixed ? columns : {
|
|
1343
1468
|
xs: "1fr",
|
|
1344
1469
|
lg: columns
|
|
@@ -1520,6 +1645,6 @@ function DashboardFilters({ layout, formatters = {}, gap = 2 }) {
|
|
|
1520
1645
|
});
|
|
1521
1646
|
}
|
|
1522
1647
|
//#endregion
|
|
1523
|
-
export { BarChart, ComboChart, DashboardCanvas, DashboardCharts, DashboardFilters, DashboardProvider, DataTable, DonutChart, FilterSelect, INSPECT_ATTRIBUTE, KpiCard, Legend, ModelDictionary, OTHER_COLOR, SERIES_COLORS, TooltipBox, chartPlaceholder, fieldLabel, formatCurrency, formatMeasure, formatNumber, measureFormat, measureFormatter, numberOf, seriesColor, useClickFilter, useDashboard, useModelQuery };
|
|
1648
|
+
export { BarChart, ComboChart, DashboardCanvas, DashboardCharts, DashboardFilters, DashboardProvider, DataTable, DonutChart, FilterSelect, INSPECT_ATTRIBUTE, KpiCard, Legend, ModelDictionary, OTHER_COLOR, SERIES_COLORS, TooltipBox, chartCardSx, chartPlaceholder, chartTitleSx, fieldLabel, formatCurrency, formatMeasure, formatNumber, measureFormat, measureFormatter, numberOf, seriesColor, useClickFilter, useDashboard, useModelQuery };
|
|
1524
1649
|
|
|
1525
1650
|
//# sourceMappingURL=index.js.map
|