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#
dependencies:
sulfite_report_manager:
git:
url: https://github.com/rafaelgazani/sulfite.git
path: packages/sulfite_report_manager
Uso rápido#
import 'package:sulfite_report_manager/sulfite_report_manager.dart';
import 'package:sulfite_studio/sulfite_studio.dart';
import 'package:sqlite3/sqlite3.dart';
final db = sqlite3.open('reports.db');
ReportManagerWidget(
repository: SqliteReportRepository.open(db),
groupRepository: SqliteReportGroupRepository.open(db),
canInsert: true,
canEdit: true,
canDelete: true,
viewerBuilder: (context, entry, definition) => SulfiteConsumerScreen(
report: definition,
title: entry.name,
dataSourceResolvers: [
PostgRestDataSourceResolver(registry: myRegistry),
RestDataSourceResolver(),
],
onExportPdf: () => _exportPdf(definition),
),
)
ReportManagerWidget#
Widget principal que renderiza a tela de gerenciamento completa (AppBar + barra de busca + lista + FAB).
Parâmetros#
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
repository |
ReportRepository |
— | Backend de armazenamento (obrigatório) |
groupRepository |
ReportGroupRepository? |
null |
Habilita aba de grupos, filtros por grupo e associação relatório↔grupo |
tenantId |
String |
defaultReportTenantId |
Tenant usado nas consultas e mutações do manager |
canInsert |
bool |
false |
Exibe FAB para criar novo relatório |
canEdit |
bool |
false |
Exibe ícone de edição em cada item da lista |
canDelete |
bool |
false |
Exibe ícone de exclusão em cada item da lista |
canEditGlobal |
bool |
false |
Permite criar/editar/excluir grupos globais |
canSettings |
bool |
false |
Exibe ícone de configurações na AppBar |
title |
String |
'Relatórios' |
Título exibido na AppBar |
onSettings |
VoidCallback? |
null |
Substituí o dialog padrão ao clicar em configurações |
viewerBuilder |
Widget Function(BuildContext, ReportEntry, ReportDefinition)? |
null |
Builder chamado ao tocar em um relatório — recebe a definição já parseada |
entryAvailabilityResolver |
ReportEntryAvailability Function(ReportEntry)? |
null |
Desabilita entradas específicas na lista (ex: bloquear relatórios PostgreSQL no Flutter Web) |
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
canEditGlobal → Administração de grupos globais
canSettings → Ícone de configurações na AppBar (onSettings ou dialog padrão)
Grupos de relatórios#
Passe um ReportGroupRepository para ativar:
- aba
Gruposno manager; - filtros horizontais por grupo em
Relatórios; - chips coloridos nos itens da lista;
- criação, edição e exclusão de grupos;
- seleção de múltiplos grupos no dialog de relatório.
O cadastro de grupo aceita uma cor opcional livre. O valor é normalizado para
RRGGBB e salvo como colorHex; na interface ele aparece como
#RRGGBB. Se a
cor estiver ausente, o manager calcula uma cor automática a partir do tema atual.
final reportRepo = RestReportRepository(baseUrl: serverUrl);
final groupRepo = RestReportGroupRepository(baseUrl: serverUrl);
ReportManagerWidget(
repository: reportRepo,
groupRepository: groupRepo,
tenantId: 'acme',
canInsert: permissions.canInsert,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete,
canEditGlobal: permissions.canEditGlobal,
viewerBuilder: (ctx, entry, definition) {
return SulfiteConsumerScreen(report: definition, title: entry.name);
},
)
entryAvailabilityResolver#
Use para desabilitar relatórios condicionalmente sem removê-los da lista — útil para bloquear relatórios que exigem recursos indisponíveis na plataforma atual (ex: PostgreSQL no Flutter Web):
ReportManagerWidget(
repository: repository,
entryAvailabilityResolver: (entry) {
// Bloqueia relatórios com datasource Postgres no Web
if (kIsWeb && entry.tags.contains('postgres')) {
return const ReportEntryAvailability.disabled(
'Não disponível no navegador — requer conexão direta com banco de dados',
);
}
return const ReportEntryAvailability.enabled();
},
viewerBuilder: (ctx, entry, def) => SulfiteConsumerScreen(report: def),
)
O item aparece na lista com estilo visual desabilitado e exibe o disabledReason ao usuário. Tocar no item não abre o visualizador.
Fluxo de visualização#
Ao tocar em um relatório na lista (com viewerBuilder fornecido):
- Abre imediatamente uma rota com loading — sem bloquear a lista
- Aguarda a animação de transição terminar (sem jank)
- Busca o conteúdo completo do relatório (
findById) em background - Parseia a
ReportDefinition - 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.
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: [
PostgRestDataSourceResolver(registry: myRegistry),
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:
viewerBuilder: (ctx, entry, definition) {
return SulfiteConsumerScreen(
report: definition,
title: entry.name,
dataSourceResolvers: [RestDataSourceResolver()],
// Quando canSettings: true no manager, exponha o botão de editar:
onEdit: 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.
final repo = InMemoryReportRepository();
await repo.insert(ReportEntryDraft(
name: 'Meu Relatório',
definitionJson: jsonEncode(myReport.toJson()),
));
SqliteReportRepository#
Persiste localmente via sqlite3. Ideal para apps mobile/desktop offline.
import 'package:sqlite3/sqlite3.dart';
final db = sqlite3.open('reports.db');
final repo = SqliteReportRepository.open(db);
RestReportRepository#
Conecta a uma API REST. Espera endpoints padrão:
| Método | Endpoint | Descrição |
|---|---|---|
GET | /reports | Lista todos |
GET | /reports?q={query} | Busca |
GET | /reports/{id} | Busca por ID (com conteúdo) |
POST | /reports | Cria |
PUT | /reports/{id} | Atualiza |
DELETE | /reports/{id} | Exclui |
final repo = RestReportRepository(baseUrl: 'https://api.example.com');
PostgresReportRepository#
Conecta diretamente ao PostgreSQL via postgres package. Recomendado para servidores/desktop apps.
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:
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:
listAllesearchretornam entradas sem conteúdo (packageBytesedefinitionJsoncomonull). SomentefindByIdretorna o conteúdo completo. O manager chamafindByIdautomaticamente antes de chamarviewerBuilder.
Modelo ReportEntry#
| Campo | Tipo | Descrição |
|---|---|---|
id | String | Identificador único |
name | String | Nome exibido na lista |
description | String? | Descrição opcional |
createdAt | DateTime | Data de criação |
updatedAt | DateTime | Data da última atualização |
storageKind |
ReportStorageKind |
package (.sulfite) ou json |
packageBytes |
Uint8List? |
Bytes do .sulfite (lazy — só em findById) |
definitionJson |
String? |
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.
// 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.
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'},
),
ConnectionEntry.http(
name: 'supabase_prod',
baseUrl: 'https://xyz.supabase.co/rest/v1',
defaultHeaders: {
'apikey': 'your-anon-key',
'Authorization': 'Bearer your-anon-key',
},
),
]);
// Passado aos resolvers — datasources com connectionRef usarão este registry
final pgResolver = PostgresDataSourceResolver(registry: registry);
final postgrestResolver = PostgRestDataSourceResolver(registry: registry);
// Adicionar ou substituir:
registry.upsert(
ConnectionEntry.database(name: 'homologacao', host: 'db-hom.example.com', ...),
);
// Remover pelo nome:
registry.remove('homologacao');
| Método | Descriçã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 |
entries | Có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
initStatedo 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.
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âmetro | Tipo | Descrição |
|---|---|---|
registry |
MutableConnectionRegistry |
Registry mutável que será exibido e editado |
onTestConnection |
Future<String?> Function(ConnectionEntry)? |
Callback de teste; retorna null em sucesso ou mensagem de erro |
Formulário de conexão
PostgreSQL
| Campo | Descrição |
|---|---|
| Nome | Identificador único — deve coincidir com connectionRef nos datasources |
| Host / Porta / Database | Endereço e banco |
| Usuário / Senha | Autenticação |
| Schema PostgreSQL | Opcional — substituído em {_schema} nas queries |
| Usar SSL | Liga/desliga SSL |
HTTP / REST
| Campo | Descrição |
|---|---|
| Nome | Identificador único |
| Base URL | URL base usada pelos datasources HTTP com connectionRef |
| Headers |
Pares chave-valor injetados em todas as requisições (ex:
apikey
,
Authorization
). Configuráveis via editor visual.
|
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:
final registry = MutableConnectionRegistry();
SulfiteDesigner(
initialReport: report,
dataSourceResolvers: [
PostgRestDataSourceResolver(registry: registry),
PostgresDataSourceResolver(registry: registry),
RestDataSourceResolver(),
],
reportManager: ReportManagerWidget(
repository: SqliteReportRepository.open(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: [
PostgRestDataSourceResolver(registry: registry),
PostgresDataSourceResolver(registry: registry),
RestDataSourceResolver(),
],
),
),
)