Skip to content

sulfite_report_manager

Pacote Flutter que oferece widgets prontos para listar, buscar, criar, editar, excluir e visualizar relatórios Sulfite em qualquer aplicação, com suporte a múltiplos backends de armazenamento e sistema de permissões granular.

Instalação

yaml
dependencies:
  sulfite_report_manager:
    git:
      url: https://github.com/rafaelgazani/sulfite.git
      path: packages/sulfite_report_manager

Uso rápido

dart
import 'package:sulfite_report_manager/sulfite_report_manager.dart';
import 'package:sulfite_studio/sulfite_studio.dart';

ReportManagerWidget(
  repository: SqliteReportRepository(db),
  canInsert: true,
  canEdit: true,
  canDelete: true,
  viewerBuilder: (context, entry, definition) => SulfiteConsumerScreen(
    report: definition,
    title: entry.name,
    dataSourceResolvers: [RestDataSourceResolver()],
    onExportPdf: () => _exportPdf(definition),
  ),
)

ReportManagerWidget

Widget principal que renderiza a tela de gerenciamento completa (AppBar + barra de busca + lista + FAB).

Parâmetros

ParâmetroTipoPadrãoDescrição
repositoryReportRepositoryBackend de armazenamento (obrigatório)
canInsertboolfalseExibe FAB para criar novo relatório
canEditboolfalseExibe ícone de edição em cada item da lista
canDeleteboolfalseExibe ícone de exclusão em cada item da lista
canSettingsboolfalseExibe ícone de configurações na AppBar
titleString'Relatórios'Título exibido na AppBar
onSettingsVoidCallback?nullSubstituí o dialog padrão ao clicar em configurações
viewerBuilderWidget Function(BuildContext, ReportEntry, ReportDefinition)?nullBuilder chamado ao tocar em um relatório — recebe a definição já parseada

Mapa de permissões

canInsert    → FAB "Novo relatório" na lista
canEdit      → Ícone de edição em cada item (editar metadados: nome/descrição)
canDelete    → Ícone de exclusão em cada item
canSettings  → Ícone de configurações na AppBar (onSettings ou dialog padrão)

Fluxo de visualização

Ao tocar em um relatório na lista (com viewerBuilder fornecido):

  1. Abre imediatamente uma rota com loading — sem bloquear a lista
  2. Aguarda a animação de transição terminar (sem jank)
  3. Busca o conteúdo completo do relatório (findById) em background
  4. Parseia a ReportDefinition
  5. Chama viewerBuilder(context, entry, definition) → exibe o widget retornado

O viewerBuilder recebe o ReportEntry completo e a ReportDefinition já parseada. O app controla tudo sobre a visualização — resolvers, botões de exportação, botão de editar no designer, etc.

dart
ReportManagerWidget(
  repository: repository,
  canInsert: isAdmin,
  canEdit: isAdmin,
  canDelete: isAdmin,
  canSettings: isAdmin,
  onSettings: () => Navigator.of(context).push(
    MaterialPageRoute(
      builder: (_) => ConnectionManagerWidget(registry: myRegistry),
    ),
  ),
  viewerBuilder: (ctx, entry, definition) {
    return SulfiteConsumerScreen(
      report: definition,
      title: entry.name,
      dataSourceResolvers: [
        PostgresDataSourceResolver(registry: myRegistry),
        RestDataSourceResolver(),
      ],
      onExportPdf: () => _exportPdf(definition),
      onExportExcel: () => _exportExcel(definition),
    );
  },
)

Abrir o Designer a partir do visualizador

Use o viewerBuilder para compor o SulfiteConsumerScreen com um botão de edição que abre o Designer:

dart
viewerBuilder: (ctx, entry, definition) {
  return SulfiteConsumerScreen(
    report: definition,
    title: entry.name,
    dataSourceResolvers: [RestDataSourceResolver()],
    // Quando canSettings: true no manager, exponha o botão de editar:
    onEditReport: isAdmin
        ? () => Navigator.of(ctx).push(
              MaterialPageRoute(
                builder: (_) => SulfiteDesigner(
                  initialReport: definition,
                  onSave: (updated) async {
                    await repository.update(
                      entry.id,
                      ReportEntryDraft(
                        name: entry.name,
                        definitionJson: jsonEncode(updated.toJson()),
                      ),
                    );
                  },
                ),
              ),
            )
        : null,
  );
}

Backends de armazenamento

O pacote fornece quatro implementações prontas de ReportRepository:

InMemoryReportRepository

Para testes, protótipos e demos. Dados não persistem entre sessões.

dart
final repo = InMemoryReportRepository();
await repo.insert(ReportEntryDraft(
  name: 'Meu Relatório',
  definitionJson: jsonEncode(myReport.toJson()),
));

SqliteReportRepository

Persiste localmente via sqflite. Ideal para apps mobile/desktop offline.

dart
final db = await openDatabase('reports.db');
final repo = SqliteReportRepository(db);

RestReportRepository

Conecta a uma API REST. Espera endpoints padrão:

MétodoEndpointDescrição
GET/reportsLista todos
GET/reports?q={query}Busca
GET/reports/{id}Busca por ID (com conteúdo)
POST/reportsCria
PUT/reports/{id}Atualiza
DELETE/reports/{id}Exclui
dart
final repo = RestReportRepository(baseUrl: 'https://api.example.com');

PostgresReportRepository

Conecta diretamente ao PostgreSQL via postgres package. Recomendado para servidores/desktop apps.

dart
final conn = await Connection.open(...);
final repo = PostgresReportRepository(conn);
await repo.migrate(); // cria a tabela se não existir

Implementar seu próprio repository

Implemente a interface ReportRepository:

dart
abstract class ReportRepository {
  Future<List<ReportEntry>> listAll();
  Future<List<ReportEntry>> search(String query);
  Future<ReportEntry?> findById(String id);
  Future<ReportEntry> insert(ReportEntryDraft draft);
  Future<ReportEntry> update(String id, ReportEntryDraft draft);
  Future<void> delete(String id);
  Future<void> dispose() async {}
}

Contrato lazy-loading: listAll e search retornam entradas sem conteúdo (packageBytes e definitionJson como null). Somente findById retorna o conteúdo completo. O manager chama findById automaticamente antes de chamar viewerBuilder.

Modelo ReportEntry

CampoTipoDescrição
idStringIdentificador único
nameStringNome exibido na lista
descriptionString?Descrição opcional
createdAtDateTimeData de criação
updatedAtDateTimeData da última atualização
storageKindReportStorageKindpackage (.sulfite) ou json
packageBytesUint8List?Bytes do .sulfite (lazy — só em findById)
definitionJsonString?JSON da definição (lazy — só em findById)

Formato .sulfite (RFC-017)

O manager suporta nativamente relatórios no formato .sulfite (pacote ZIP com definição + assets embutidos). Ao abrir um relatório com storageKind: package, o SulfitePackageReader extrai a definição automaticamente.

dart
// Importar um .sulfite existente
final bytes = await File('relatorio.sulfite').readAsBytes();
await repo.insert(ReportEntryDraft(
  name: 'Meu Relatório',
  packageBytes: bytes,
));

Gerenciar conexões em runtime

O pacote exporta MutableConnectionRegistry e ConnectionManagerWidget para que o app consumidor possa adicionar, editar e remover conexões de banco de dados ou HTTP em tempo de execução — sem reiniciar o app.

MutableConnectionRegistry

ChangeNotifier que implementa ConnectionRegistry. Usado como fonte de conexões nomeadas pelos resolvers do sulfite_datasources.

dart
final registry = MutableConnectionRegistry([
  ConnectionEntry.database(
    name: 'producao',
    host: 'db.example.com',
    port: 5432,
    database: 'app_db',
    username: 'app_user',
    password: 'secret',
    ssl: true,
    extra: {'schema': 'public'},
  ),
]);

// Passar para o resolver — os datasources com connectionRef usarão este registry
final resolver = PostgresDataSourceResolver(registry: registry);

// Adicionar ou substituir:
registry.upsert(
  ConnectionEntry.database(name: 'homologacao', host: 'db-hom.example.com', ...),
);

// Remover pelo nome:
registry.remove('homologacao');
MétodoDescrição
upsert(entry)Adiciona ou substitui a conexão com o mesmo name e notifica listeners
remove(name)Remove pelo nome; não-op se não existir
entriesCópia imutável da lista atual
resolve(ref)Busca conexão pelo name (implementa ConnectionRegistry)

Persistência: as conexões ficam em memória. Para persistir entre sessões, salve e restaure a lista no initState do app.

ConnectionManagerWidget

Tela pronta (Scaffold + AppBar + lista + FAB) para gerenciar as conexões de um MutableConnectionRegistry. Exibe conexões PostgreSQL e HTTP/REST com ações de adicionar, editar, remover e testar.

dart
ConnectionManagerWidget(
  registry: myRegistry,
  onTestConnection: (entry) async {
    // Retorne null se OK, ou uma mensagem de erro
    try {
      await PostgresDataSourceResolver(
        registry: MutableConnectionRegistry([entry]),
      ).resolve(
        DataSource(
          id: '_ping', type: 'list', schema: {},
          source: 'external', connectionRef: entry.name,
          query: 'SELECT 1',
        ),
      );
      return null;
    } catch (e) {
      return e.toString();
    }
  },
)

Parâmetros

ParâmetroTipoDescrição
registryMutableConnectionRegistryRegistry mutável que será exibido e editado
onTestConnectionFuture<String?> Function(ConnectionEntry)?Callback de teste; retorna null em sucesso ou mensagem de erro

Formulário de conexão

PostgreSQL

CampoDescrição
NomeIdentificador único — deve coincidir com connectionRef nos datasources
Host / Porta / DatabaseEndereço e banco
Usuário / SenhaAutenticação
Schema PostgreSQLOpcional — substituído em {_schema} nas queries
Usar SSLLiga/desliga SSL

HTTP / REST

CampoDescrição
NomeIdentificador único
Base URLURL base usada pelos datasources HTTP com connectionRef

O nome de uma conexão é imutável após criado — serve como chave de lookup. Para "renomear", remova a antiga e crie uma nova.

Integrando com SulfiteDesigner

Passe o ConnectionManagerWidget via onSettings do manager ou diretamente no slot reportManager do designer:

dart
final registry = MutableConnectionRegistry();

SulfiteDesigner(
  initialReport: report,
  dataSourceResolvers: [
    PostgresDataSourceResolver(registry: registry),
    RestDataSourceResolver(),
  ],
  reportManager: ReportManagerWidget(
    repository: SqliteReportRepository(db),
    canInsert: true,
    canEdit: true,
    canDelete: true,
    canSettings: true,
    onSettings: () => Navigator.of(context).push(
      MaterialPageRoute(
        builder: (_) => ConnectionManagerWidget(registry: registry),
      ),
    ),
    viewerBuilder: (ctx, entry, def) => SulfiteConsumerScreen(
      report: def,
      title: entry.name,
      dataSourceResolvers: [
        PostgresDataSourceResolver(registry: registry),
      ],
    ),
  ),
)

Sulfite do 🇧🇷 para o mundo © 2026 Rafael S. Pinheiro