Camada de Apresentação

Introdução

A camada de apresentação é composta dos componentes React que representam as páginas web da aplicação.

Cada página é um componente React declarado em módulo JavaScript de nome page.js, conforme convenções do AppRouter do Next.js.

A rota, ou path da URL da página, é estabelecida conforme a estrutura hierárquica das pastas onde o arquivo page.js está localizado.

A página é instanciada dentro de um template, defindo no arquivo layout.js. O Next.js aplica o layout que está mais próximo do arquivo page.js, em termos de estrutura de pastas. Todo layout é instanciado, por sua vez, dentro de um layout de hierarquia superior, até que tudo esteja abraçado pela layout raiz da aplicação.

Consulte o manual do Next.js para mais informações sobre páginas, layouts, rotas e outras funcionalidades do framework.

Estrutura de Arquivos

As páginas web, nos padrões da presente arquitetura, ficam localizadas dentro da pasta app/(pages), dedicada à camada de apresentação.

Segure-se que a camada de apresentação tenha a seguinte estrutura interna:

layout.js                     (*1)
error.js
(client)/
   layout.js                  (*2)
   (protected)/
      layout.js               (*3)
      [...]                   (*4)
   (public)/
      [...]                   (*5)

(*1) Layout raiz da aplicação, que contém a estrutura HTML estática (tags <html>, <head> e <body>), sem componentes React. Neste arquivo são importados os resources globais, essencialmente os arquivos CSS.

(*2) Layout dedicado à parte que é carregada no lado cliente (browser). Contém componentes React globais da aplicação. Envolve essencialmente os elementos estruturantes do Cloudsupport for React. Consulte a seção Configuração.

(*3) Layout aplicado nas páginas protegidas, ou seja, em que o usuário foi autenticado com sucesso. Contém elementos como cabeçalho, rodapé, menu principal e menu do usuário.

(*4) Todas as páginas protegidas são colocadas na pasta (protected) e suas subpastas.

(*5) Diretório dedicado às páginas públicas, como eventual landing page e página de pós logout.

Lembrete: Pelo Next.js, diretórios com parânteses não determinam path na rota (URL) das páginas. Eles são utilizados como organizadores do código-fonte, como é o caso das pastas (protected), que agrupa as páginas de acesso restrito (autenticado), e (public), que agrupa as páginas sem requisito de autenticação. A pasta (client) apenas indica que todo seu conteúdo é client-side, ou seja, processado no lado do cliente (o browser). Entrentanto, os diretórios com parânteses são considerados na composição do layout. Ou seja, páginas dentro de (protected) são abraçadas pelo layout (3), que por sua vez é abraçado pelo layout (2) e por fim pelo layout raiz (1).

Exemplo de conteúdo da pasta (protected), que contém as páginas com acesso restrito:

layout.js
ajustes/
   page.js
apoio/
   page.js
executores/
   page.js
home/
   page.js                        (*1)
   pageAcaoAtestar.js
   pageAcaoCancelar.js
   pageAcaoEncaminhar.js
   pageAcaoExecutar.js
   pageAcaoGerar.js
   pageAcaoGlosar.js
   pageAcaoExecutar.js
   pageTableColumnDataAbertura.js
   pageTableColumnProtocolo.js
   pageTableColumnSituacao.js
   pageTableMenuContexto.js
   pageTableRowExpansion.js

O exemplo acima mapeia quatro páginas, nos endereços: /ajutes, /apoio, /executores e /home.

(*1) Esta página foi dividada em módulos para melhor legibilidade e manutenção do código. Foram criados componentes React para tratar cada ação transacional da página, referenciados nos botões e menu de contexto, e componentes/funções utilizados na tabela principal (para células de colunas, menu de contexto e expansão de linha).

Note que os componentes da página /home seguem um padrão em que os nomes dos arquivos são prefixados pelo tipo de componente no qual serão inseridos na página.

Exemplo de conteúdo da pasta (public), que contém as páginas com acesso sem restrição:

(index)/
   page.js
loggedOut/
   page.js

O diretório “(index)” contém uma landing page, mapeada no path raiz (/). O diretório “loggedOut”, de path /loggedOut, contém a página a ser exibida após o logout com sucesso.

Páginas

Implementar cada página web na forma de um componente React, conforme as diretrizes:

  • Evitar a implementação de regras de negócio, que é função do backend, com exceção de redundâncias para fins de UX, especialmente validações de formulários.

  • Evitar componentes React com mais de 200 linhas de código.

    • Prefira dividí-los em componentes menores. Para melhor legibilidade, estes componentes ficam localizados no mesmo diretório da página.

    • Cada componente adicional deve ser declarado em arquivo JavaScript distinto e exportado de maneira nomeada. O export default é utilizado tão somente quando requisitado pelo Next.js.

  • Não consumir webservices diretamente na página.

    • Deve-se implementar o consumo na camada de integração, na forma de serviço remoto, e importar a função correspondente na página. Consulte a seção de Integração para mais informações.

Sugere-se o seguinte template de código para um componente React de página:

'use client'
import React from 'react';

// Demais imports

// Documentação dos parâmetros de URL, caso se aplicarem
export default function Page() {
  
    // Hooks do Next

    // Hooks do Cloudsupport

    // Refs
    
    // States

    // Funções de apoio

    // Funções de eventos de UI

    // Effects

    return (
        <>            
            {/* Elementos da página */}
        </>
    );
}

Componentes de UI

A presente arquitetura é agnóstica quanto ao framework de componentes de interface gráfica. Sugere-se o:

  • Mantine: Para aplicações de interface de menor complexidade e mais focadas em responsividade. O Mantine possui mais de 100 componentes, com visual elegante e moderno, 50 Hooks para simplificar tarefas comuns, gerenciamento de tema e uma solução validação de formulários.

  • PrimeReact: Para aplicações de uso geral. O PrimeReact possui uma ampla quantidade de componentes, que inclui tabelas avançadas, sendo uma opção conveniente para aplicações de interface gráfica mais complexa, como por exemplo sistemas de gestão empresarial (ERP, Enterprise resource planning). O PrimeReact oferece um conjunto de Hooks para simplificar tarefas comuns. Quanto à validação de formulários, sugere-se um framework complementar, a citar o React Hook Form.

  • PatternFly: Para aplicações de uso geral ou que tenham caráter mais técnico. Há componentes para exibição de código-fonte, visualização de logs e diagramas de topologia. É um framework rico em opções visuais. Inclui guia de boas práticas de UX, incluindo acessibilidade.

O Cloudsupport for React considera o PrimeReact em seus exemplos. Também é utilizado o PrimeFlex, associado ao PrimeReact, para fins de layout e formatação padronizada, sem necessidade de declaração explícita de estilos CSS.

O Cloudsupport for React oferece alguns componentes de UI suplementares para PrimeReact:

  • Componentes relativos a controle de acesso, a serem declarados no layout da aplicação:

  • Componentes relativos a controle de acesso, a serem utilizados em páginas:

    • LoggedOut: Exibe aviso de sessão encerrada.
  • Componente relativo à detecção de nova versão da aplicação, a ser declarado no layout:

    • ReloadBanner: Exibe aviso de que nova versão da aplicação está disponível.
  • Componentes relativos a indicação de processamento:

    • ProcessingIndicator: Exibe indicador global de processamento.
    • RefreshingIcon: Exibe ícone processamento, para uso em locais específicos da página.
    • LoadingBar: Exibe uma barra de progresso, para uso em locais específicos da página.
  • Outros componentes:

    • Box: Aplica uma margem padrão no conteúdo.
    • ContextBar: Um Toobar com espaço para o título da página.
    • Field: Permite alinhar campos de valor ou de formulários, com responsividade.
    • InlineField: Permite alinhar horizontalmente campos de valor ou de formulários.
    • RowExpansionBox: Um container para uso dentro de RowExpansion de tabelas.
    • MenuButton: Um botão com estilo indicado para uso dentro de células de tabelas.
    • MessageBlock: Exibe uma mensagem em tela cheia.
    • ErrorMessage: Exibe uma mensagem de erro.

O Cloudsupport for React oferece também os seguintes componentes de UI, que não dependem o PrimeReact:

A seção Documentação da API contém a documentação dos componentes de UI.

A seção Configuração contém a documentação sobre como habilitar o Cloudsupport for React no projeto, necessário para que os recursos e componentes da arquitetura funcionem.

Hooks

O Cloudsupport for React oferece alguns Hooks:

  • useAuth: Acesso aos dados do usuário OIDC autenticado e API para login/logout.
  • useAccessToken: Acesso ao AccessToken, usualmente o JWT.
  • hasAuthority: Protege elementos contra acesso não autorizado OIDC.
  • hasAnyAuthority: Protege elementos contra acesso não autorizado OIDC.
  • useProfile: Acesso às propriedades de ambiente da aplicação.
  • useProfileStatus: Indica se a aplicação está atualizada.
  • useProcessing: Acesso à API para indicação de processamento, permite feedback visual para o usuário.
  • useGlobal: Acesso a um estado globa, para uso livre em escopo do aplicação.

Para aplicações que utilizam o PrimeReact, o Cloudsupport for React oferece o seguinte Hook adicional:

  • useToast: Acesso à API global para exibição de notificações (info, warning, error).

A seção Documentação da API contém a documentação dos Hooks.

A seção Configuração contém a documentação sobre como habilitar o Cloudsupport for React no projeto, necessário para que os recursos e Hooks da arquitetura funcionem.

Exemplos

O projeto de arquétipo web implementa um exemplo de aplicação de atendimento a ordens de serviços. O arquétipo contém quatro exemplos de página web, cada exemplo em determinado nível de complexidade, citados a seguir.

Nível 1: Página estática

  • Página estática que mostra as configurações da aplicação.

Screenshot:

Página Nível 1

Código-fonte:

export default function Page() {
  
    const profile = useProfile();               // Hook que obtém o profile da aplicação
    const atributos = Object.keys(profile);     // Lista os campos do objeto profile

    return (
        <>
            <ContextBar title='Configurações da Aplicação' />
            
            <Box>
                {atributos.map(at => 
                <Field key={at} label={at}>{JSON.stringify(profile[at])}</Field>)}
            </Box>
        </>
    );
}

Hook useProfile e componentes Box, Field e ContextBar são oferecidos pela arquitetura. Consulte mais informações na documentação da API.

O cabeçalho, que contém logo, menu principal e menu do usuário, está declarado no arquivo layout.js.

Nível 2: Consulta de dados

  • Página com consumo de webservices para consulta de dados remotos.

  • Exibição em abas.

Screenshot:

Página Nível 2

Código-fonte:

export default function Page() {
  
    const processing = useProcessing(); // Indicador global de processamento do Cloudsupport
    const toast = useToast();           // Toast global de notificações do Cloudsupport

    const [situacoesOs, setSituacoesOs] = React.useState();
    const [motivosCancelamento, setMotivosCancelamento] = React.useState();
    const [tiposExecutor, setTiposExecutor] = React.useState();

    // Carrega a página consultando os dados remotos
    const carregar = () => {
        processing.notifyStart();

        // Realiza as três consultas no backend (o browser pode eventualmente paralelizar)
        const promise1 = pesquisarSituacoesOs().then(setSituacoesOs);
        const promise2 = pesquisarMotivosCancelamento().then(setMotivosCancelamento);
        const promise3 = pesquisarTiposExecutores().then(setTiposExecutor);

        Promise.all([promise1, promise2, promise3]) // Aguarda a conclusão dos 3 requests
        .catch(toast.showError)
        .finally(processing.notifyEnd);
    }

    // Carregamento inicial da página
    useMountEffect(carregar);
    
    return (
        <>
            <ContextBar title='Consulta de Tabelas de Apoio' />
                
            <TabView>
                <TabPanel header="Situações de OS">
                    {situacoesOs && situacoesOs.map(i => 
                    <Field key={i.id}>{i.id} - {i.descricao}</Field>)}
                </TabPanel>
                <TabPanel header="Motivos de Cancelamento de OS">
                    {motivosCancelamento && motivosCancelamento.map(i => 
                    <Field key={i.id}>{i.id} - {i.descricao}</Field>)}
                </TabPanel>
                <TabPanel header="Tipos de Executor">
                    {tiposExecutor && tiposExecutor.map(i => 
                    <Field key={i.id}>{i.id} - {i.descricao}</Field>)}
                </TabPanel>
            </TabView>
        </>
    );
}

Hooks useProcessing e useToast e componentes Field e ContextBar são oferecidos pela arquitetura. Consulte mais informações na documentação da API.

Hook useMountEffect e componentes TabView e TabPanel são do PrimeReact.

O cabeçalho, que contém logo, menu principal e menu do usuário, está declarado no arquivo layout.js.

Nível 3: Alteração de dados, tabela e botões

  • Página com consumo de webservices para consulta e alteração de dados remotos.

  • Exibição em tabela simples.

  • Botões de ação.

Screenshot:

Página Nível 3

Código-fonte:

export default function Page() {
  
    const processing = useProcessing(); // Indicador global de processamento do Cloudsupport
    const toast = useToast();           // Toast global de notificações do Cloudsupport
    const [executores, setExecutores] = React.useState();  // Lista de executores

    // Carrega a página consultando os dados remotos
    const carregar = () => {
        processing.notifyStart();

        pesquisarExecutores()
        .then(setExecutores)
        .catch(toast.showError)
        .finally(processing.notifyEnd);
    }

    // Evento do botão que inativa o executor
    const inativar = (executor) => {
        confirmDialog({
            message: 'Deseja inativar este executor?',
            header: 'Confirmação',
            icon: 'pi pi-info-circle',
            acceptLabel: 'Confirmar',
            rejectLabel: 'Cancelar',
            accept: () => {

                processing.notifyStart();

                inativarExecutor({ uid: executor.uid })
                .then(() => toast.showInfo(`Executor ${executor.nome} foi inativado.`))
                .catch(toast.showError)
                .then(pesquisarExecutores) // Sempre recarrega os registros
                .then(setExecutores)
                .catch(toast.showError)
                .finally(processing.notifyEnd);
                
            },
        });
    }

    // Evento do botão que reativa o executor
    const reativar = (executor) => {
        // (ocultado por similaridade ao método acima)
    }

    // Carregamento inicial da página
    useMountEffect(carregar);

    // Renderiza a coluna com menu de ações
    const renderColumnMenu = (executor) => 
    <>
        {executor.dataInativacao &&
        <Button label='Ativar' size="small" severity="success" onClick={() => reativar(executor)} />}

        {!executor.dataInativacao &&
        <Button label='Inativar' size="small" severity="warning" onClick={() => inativar(executor)} />}
    </>;

    return (
        <>
            <ContextBar title='Executores' />
                
            {executores &&
            <DataTable value={executores.lista} emptyMessage="Nenhum executor cadastrado">
                <Column header="Nome do executor" field="nome" />
                <Column header="Tipo"             field="tipoExecutorDescricao" />
                <Column header="Inativado em"     body={item => fmtDate(item.dataInativacao, 'datetime')} />
                <Column                           body={item => renderColumnMenu(item)} className='w-1rem' />
            </DataTable>}
        </>
    );
}

Hooks useProcessing e useToast e componente ContextBar são oferecidos pela arquitetura. Consulte mais informações na documentação da API.

Hook useMountEffect e componentes DataTable, Column, Button e confirmDialog são do PrimeReact.

O cabeçalho, que contém logo, menu principal e menu do usuário, está declarado no arquivo layout.js.

Nível 4: Paginação, menu, popup, formulário

O exemplo de nível 4 inclui os recursos do nível 3, mais:

  • Paginação em backend (“Lazy” do PrimeReact).

  • Expansão de linha.

  • Customização de células de tabela (componente React em vez de texto simples).

  • Pesquisa com filtro.

  • Menu de contexto com ações em três (3) níveis de complexidade:

    • Ação nível 1: Apenas solicita uma confirmação do usuário antes de executar a ação.

    • Ação nível 2: Popup com formulário simples e botões de ação.

    • Ação nível 3: Popup com carregamento de dados sob demanda.

Screenshot:

Página Nível 4

Observação: No momento, orienta-se no sentido de utilizar o framework React Hook Form para validação de formulários. Ressalta-se que as validações, ainda que implementadas na camada de apresentação, devem ser implementadas no backend.

Código-fonte:

export default function Page() {
  
    const processing = useProcessing(); // Indicador global de processamento do Cloudsupport
    const toast = useToast();           // Toast global de notificações do Cloudsupport

    const acaoGerar = React.useRef();
    const acaoEncaminhar = React.useRef();
    const acaoExecutar = React.useRef();
    const acaoCancelar = React.useRef();
    const acaoAtestar = React.useRef();
    const acaoGlosar = React.useRef();
    const menuContexto = React.useRef();
    const refreshing = React.useRef();

    const [filtroDescricao, setFiltroDescricao] = React.useState(''); // Filtro para pesquisa de ordens de serviço
    const [ordensServico, setOrdensServico] = React.useState();       // Lista de ordens de serviço
    const [osSelecionada, setOsSelecionada] = React.useState();       // Ordem de serviço selecionada ao clicar no menu
    const [expandedRows, setExpandedRows] = React.useState();         // Necessário para rowExpansion
    const [lazy, setLazy] = React.useState({first: 0, rows: 8});      // Necessário para lazy loading

    // Carrega a página consultando os dados remotos
    const carregar = () => {
        processing.notifyStart();  // Indicador geral de processamento
        refreshing.current.show(); // Animação para requisições rápidas, feedback importante de UX devido ao botão de pesquisa

        pesquisarOrdensServico({ de: lazy.first, qtd: lazy.rows, descricao: filtroDescricao })
        .then(setOrdensServico)
        .catch(toast.showError)
        .finally(processing.notifyEnd)
        .finally(refreshing.current.hide);
    }

    // Evento do botão que abre o menu de contexto
    const exibirMenuContexto = (event, ordemServico) => {
        setOsSelecionada(ordemServico);
        menuContexto.current?.show(event);
    };

    // Evento executado no lazy loading da tabela
    useUpdateEffect(carregar, [lazy]);

    // Carregamento inicial da página
    useMountEffect(carregar);

    return (
        <>
            {/* Barra de contexto */}

            <ContextBar title='Ordens de Serviço' 
                start={
                <>
                    <InputText 
                        placeholder='Buscar pela descrição' size={30} 
                        onChange={e => setFiltroDescricao(e.target.value)} 
                        onKeyDown={e => e.key === 'Enter' && carregar()} />
                    <Button icon="fa fa-search" size='small' onClick={carregar} />
                    <RefreshingIcon ref={refreshing} />
                </>}
                end={<Button label='Nova ᶜ²' icon='fa fa-plus' onClick={() => acaoGerar.current.iniciar()}/>}
            />
                
            {/* Tabela principal */}

            {ordensServico &&
            <DataTable value={ordensServico.lista} 
                emptyMessage="Nenhuma ordem de serviço cadastrada"
                rowExpansionTemplate={renderRowExpansion}                                    /* RowExpansion */
                expandedRows={expandedRows} onRowToggle={e => setExpandedRows(e.data)}       /* RowExpansion */
                paginator rows={lazy.rows}                                                   /* Paginator */
                lazy first={lazy.first} totalRecords={ordensServico.total} onPage={setLazy}  /* Lazy loading */
            >
                <Column expander={true} className='w-1rem' />
                <Column header="Abertura"    body={renderColumnDataAbertura} />
                <Column header="Protocolo"   body={renderColumnProtocolo} />
                <Column header="Descrição"   field="descricao" />
                <Column header="Situação"    body={renderColumnSituacao} />
                <Column                      body={item => 
                                                <MenuButton onClick={e => exibirMenuContexto(e, item)} />
                                             } className='w-1rem' />
            </DataTable>}

            {/* Componentes que executam ação transacional, usados no menu de contexto e na barra de contexto */}

            <AcaoGerar ref={acaoGerar} onComplete={carregar} />
            <AcaoEncaminhar ref={acaoEncaminhar} onComplete={carregar} />
            <AcaoExecutar ref={acaoExecutar} onComplete={carregar} />
            <AcaoCancelar ref={acaoCancelar} onComplete={carregar} />
            <AcaoAtestar ref={acaoAtestar} onComplete={carregar} />
            <AcaoGlosar ref={acaoGlosar} onComplete={carregar} />

            {/* Menu de contexto para a tabela */}

            <MenuContexto ref={menuContexto} 
                osSelecionada={osSelecionada}
                acaoEncaminhar={acaoEncaminhar}  
                acaoExecutar={acaoExecutar}  
                acaoCancelar={acaoCancelar}  
                acaoAtestar={acaoAtestar}  
                acaoGlosar={acaoGlosar} />
        </>
    );
}

Consulte no arquétipo o código-fonte das funções e componentes referenciados no exemplo acima.

O cabeçalho, que contém logo, menu principal e menu do usuário, está declarado no arquivo layout.js.

Note que a estrutura de componentes do page.js é enxuta para a quantidade de funcionalidades presentes (8 funções transacionais, paginação, expansão de linha, popups e menus). Isso se deve à divisão da página em componentes internos, a citar:

  • Componente para a barra de contexto.

  • Componentes para células da tabela.

  • Componente para a expansão de linha.

  • Um componente para cada ação do usuário.

  • Componente do menu de contexto.

É fundamental organizar o código-fonte em componentes pequenos para garantir uma boa manutenção da aplicação e mitigar bugs na interface do usuário.

Outros Exemplos

Além dos exemplos de páginas apresentados acima, o projeto de arquétipo inclui:

  • Exemplo de página pública de landing.

  • Exemplo de página pública pós logoff.

  • Exemplo de páginas autenticadas por SSO OIDC.

  • Exemplo de template via PrimeFlex e sem imagens (somente CSS).

  • Exemplo de aplicação de favicon multicanal (Android, iOS e desktop) e logo SVG.

  • Exemplo de menu do usuário autenticado.

  • Exemplo de configuração mínima do Cloudsupport for React que ativa os recursos da arquitetura, em (client)/layout.js.

  • Exemplo de página de erro customizada.

  • Exemplo de Dockerização.

Próximos Passos

A próxima leitura sugerida é a seção Integração, que detalha o consumo de serviços do backend.

A seção Documentação da API contém a referência completa de todos os módulos e componentes da arquitetura.