Cómo creamos informes dinámicos en SSRS 2014


Nosotros ya hablamos de cómo hemos ayudado a una empresa de fabricación de transformar los procesos de formación corporativa y el desarrollo personal. Los empleados del cliente, que se estaban ahogando en documentos en papel y hojas de cálculo Excel, recibieron una práctica aplicación para iPad y un portal web. Una de las funciones más importantes de este producto es la creación de informes dinámicos mediante los cuales los gerentes juzgan el trabajo de los empleados "en el campo". Estos son documentos enormes con docenas de campos y tamaños promedio de 3000 * 1600 píxeles.

En este artículo, hablaremos sobre cómo implementar esta belleza basada en Microsoft SQL Server Reporting Services, por qué un backend puede ser un mal amigo del portal web y qué trucos ayudarán a establecer su relación. La parte comercial completa de la solución ya se describió en el artículo anterior, por lo que aquí nos centramos en cuestiones técnicas. ¡Empecemos!


Formulación del problema


Tenemos un portal con el que trabajan varios cientos de usuarios. Están organizados en una jerarquía gradual, donde cada usuario tiene un supervisor de un rango más alto. Esta diferenciación de derechos es necesaria para que los usuarios puedan crear eventos con cualquier empleado subordinado. Puedes saltar los pasos, es decir el usuario puede comenzar la actividad con un empleado de cualquier rango inferior a él.

¿Qué eventos se entienden aquí? Esto puede ser capacitación, soporte o certificación de un empleado de una empresa comercial, que el supervisor lleva a cabo en un punto de venta. El resultado de tal evento es un cuestionario completado en un iPad con calificaciones de los empleados para las cualidades y habilidades profesionales.

Según los datos del cuestionario, puede preparar estadísticas, por ejemplo:

  • ¿Cuántos eventos con sus subordinados de este tipo creó Vasya Ivanov en un mes? ¿Cuántos de ellos están completos?
  • ¿Cuál es el porcentaje de calificaciones satisfactorias? ¿Qué preguntas responden los comerciantes a lo peor? ¿Qué gerente es peor para tomar exámenes?

Dichas estadísticas están contenidas en Informes que pueden crearse a través de la interfaz web, en formatos XLS, PDF, DOCX e imprimirse. Todas estas funciones están diseñadas para gerentes en diferentes niveles.

El contenido y el diseño de los informes se definen en las plantillas , lo que le permite establecer los parámetros necesarios. Si en el futuro los usuarios necesitarán nuevos tipos de informes, el sistema tiene la capacidad de crear plantillas, especificar parámetros modificables y agregar una plantilla al portal. Todo esto, sin interferir con el código fuente y los procesos de trabajo del producto.

Especificaciones y limitaciones


El portal se ejecuta en arquitectura de microservicio, el frente está escrito en Angular 5. El recurso utiliza la autorización JWT, es compatible con los navegadores Google Chrome, Firefox, Microsoft Edge e IE 11 .

Todos los datos se almacenan en MS SQL Server 2014. SQL Server Reporting Services (SSRS) está instalado en el servidor, el cliente lo usa y no lo va a rechazar. De ahí la limitación más importante: el acceso a SSRS está cerrado desde el exterior, por lo que puede acceder a la interfaz web y SOAP solo desde la red local a través de la autorización NTLM.

Algunas palabras sobre SSRS
SSRS – , , . docs.microsoft.com, SSRS (API) ( Report Server, - HTTP).

Atención, la pregunta: ¿cómo completar la tarea sin métodos manuales, con recursos mínimos y beneficios máximos para el cliente?

Como el cliente tiene SSRS en un servidor dedicado, deje que SSRS haga todo el trabajo sucio de generar y exportar informes. Entonces no tenemos que escribir nuestro propio servicio de informes, exportar módulos a XLS, PDF, DOCX, HTML y la API correspondiente.

Por lo tanto, la tarea era hacer amigos de SSRS con el portal y garantizar el funcionamiento de las funciones especificadas en la tarea. Así que veamos la lista de estos escenarios: se encontraron sutilezas interesantes en casi todos los puntos.

Estructura de la solución


Como ya tenemos SSRS, existen todas las herramientas para administrar plantillas de informes:

  • Servidor de informes: responsable de toda la lógica de trabajar con informes, su almacenamiento, generación, administración y mucho más.
  • Administrador de informes: un servicio con una interfaz web para administrar informes. Aquí puede cargar plantillas creadas en las herramientas de datos de SQL Server en el servidor, configurar los derechos de acceso, las fuentes de datos y los parámetros (incluidos los que se pueden cambiar al informar solicitudes). Es capaz de generar informes sobre plantillas descargadas y subirlas a varios formatos, incluidos XLS, PDF, DOCX y HTML.

Total: creamos plantillas en las herramientas de datos de SQL Server, con la ayuda de Report Manager las completamos en Report Server, configuramos y está listo. Podemos generar informes, cambiar sus parámetros.

La siguiente pregunta: ¿cómo solicitar la generación de informes sobre plantillas específicas a través del portal y obtener el resultado al frente para enviarlo a la interfaz de usuario o descargarlo en el formato deseado?

Informes de SSRS al portal


Como dijimos anteriormente, SSRS tiene su propia API para acceder a los informes. Pero no queremos entregar sus funciones por razones de seguridad e higiene digital: solo necesitamos solicitar datos de SSRS en la forma correcta y transmitir el resultado al usuario. La gestión de informes estará a cargo de personal de atención al cliente especialmente capacitado.

Dado que el acceso a SSRS es solo desde la red local, el intercambio de datos entre el servidor y el portal es a través de un servicio proxy.


Intercambio de datos entre el portal y el servidor

Veamos cómo funciona y por qué ReportProxy está aquí.

Entonces, en el lado del portal, tenemos un ReportService, al que accede el portal para informes. El servicio verifica la autorización del usuario, el nivel de sus derechos, convierte los datos de SSRS en la forma deseada según el contrato.

ReportService API contiene solo 2 métodos, que son suficientes para nosotros:

  1. GetReports : proporciona los identificadores y nombres de todas las plantillas que el usuario actual puede recibir;
  2. GetReportData (formato, parámetros) : proporciona datos de informes exportados y listos para usar en el formato especificado, con un conjunto dado de parámetros.

Ahora necesita estos 2 métodos para poder comunicarse con SSRS y tomar los datos necesarios de la forma correcta. De la documentación se sabe que podemos acceder al servidor de informes a través de HTTP utilizando la API SOAP. Parece que el rompecabezas se está desarrollando ... Pero, de hecho, una sorpresa nos espera aquí.

Dado que SSRS está cerrado al mundo exterior y solo se puede acceder a través de la autenticación NTLM, no está disponible directamente desde el portal SOAP. También hay nuestros propios deseos:

  • Otorgue acceso solo al conjunto de funciones requerido e incluso prohíba el cambio;
  • Si tiene que cambiar a otro sistema de informes, las ediciones en ReportService deben ser mínimas, y es mejor que no sean necesarias.

Aquí es donde ReportProxy nos ayuda, que se encuentra en la misma máquina que SSRS y es responsable de enviar las solicitudes de ReportService a SSRS. El procesamiento de la solicitud es el siguiente:

  1. el servicio recibe una solicitud de ReportService, verifica la autorización JWT;
  2. de acuerdo con el método API, el proxy pasa por el protocolo SOAP en SSRS para obtener los datos necesarios, iniciando sesión a través de NTLM en el camino;
  3. Los datos recibidos de SSRS se envían de nuevo a ReportService en respuesta a la solicitud.

De hecho, ReportProxy es un adaptador entre SSRS y ReportService.
El controlador es el siguiente:
[BasicAuthentication]
public class ReportProxyController : ApiController
{
    [HttpGet()]
    public List<ReportItem> Get(string rootPath)
    {
        //  ...
    }

    public HttpResponseMessage Post([FromBody]ReportRequest request)
    {
        //  ...
    }
}

BasicAuthentication :

public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        var authHeader = actionContext.Request.Headers.Authorization;

        if (authHeader != null)
        {
            var authenticationToken = actionContext.Request.Headers.Authorization.Parameter;
            var tokenFromBase64 = Convert.FromBase64String(authenticationToken);
            var decodedAuthenticationToken = Encoding.UTF8.GetString(tokenFromBase64);
            var usernamePasswordArray = decodedAuthenticationToken.Split(':');
            var userName = usernamePasswordArray[0];
            var password = usernamePasswordArray[1];

            var isValid = userName == BasiAuthConf.Login && password == BasiAuthConf.Password;

            if (isValid)
            {
                var principal = new GenericPrincipal(new GenericIdentity(userName), null);
                Thread.CurrentPrincipal = principal;

                return;
            }
        }

        HandleUnathorized(actionContext);
    }

    private static void HandleUnathorized(HttpActionContext actionContext)
    {
        actionContext.Response = actionContext.Request.CreateResponse(
            HttpStatusCode.Unauthorized
        );

        actionContext.Response.Headers.Add(
            "WWW-Authenticate", "Basic Scheme='Data' location = 'http://localhost:"
        );
    }
}


Como resultado, el proceso se ve así:

  1. El frente envía una solicitud http a ReportService;
  2. ReportService envía una solicitud http a ReportProxy;
  3. ReportProxy a través de la interfaz SOAP recibe datos de SSRS y envía el resultado a ReportService;
  4. ReportService trae el resultado de acuerdo con el contrato y se lo entrega al cliente.

Tenemos un sistema de trabajo que solicita una lista de plantillas disponibles, va a SSRS para obtener informes y las entrega al frente en cualquier formato compatible. Ahora debe mostrar los informes generados en el frente de acuerdo con los parámetros especificados, dar la oportunidad de cargarlos en archivos XLS, PDF, DOCX e imprimir. Comencemos con la pantalla.

Trabajar con informes SSRS en el portal


A primera vista, es un asunto cotidiano: el informe viene en formato HTML, por lo que podemos hacer lo que queramos con él. Lo incrustaremos en la página, lo matizaremos con estilos de diseño, y la cosa está en el sombrero. De hecho, resultó que había suficientes dificultades.

Según el concepto de diseño, la sección de informes en el portal debe constar de dos páginas:

1) una lista de plantillas donde podemos:

  • Ver estadísticas sobre actividades para todo el portal;
  • ver todas las plantillas disponibles para nosotros;
  • haga clic en la plantilla deseada y vaya al generador de informes correspondiente.



2) un generador de informes que nos permite:

  • establecer parámetros de plantilla y crear un informe sobre ellos;
  • ver lo que sucedió como resultado;
  • seleccione el formato del archivo de salida, descárguelo;
  • imprima el informe de forma conveniente y visual.



No hubo problemas especiales con la primera página, por lo que no la consideraremos más. Y el generador de informes nos obligó a encender el ingeniero, por lo que sería conveniente para personas reales usar todas las funciones en TK.

Problema número 1. Mesas gigantes


Según el concepto de diseño, esta página debe tener un área de visualización para que el usuario pueda ver su informe antes de exportarlo. Si el informe no cabe en la ventana, puede desplazarse horizontal y verticalmente. Al mismo tiempo, un informe típico puede alcanzar tamaños de varias pantallas, lo que significa que debemos pegar bloques con los nombres de filas y columnas. Sin esto, los usuarios tendrán que regresar constantemente a la parte superior de la tabla para recordar qué significa una celda en particular. O, en general, será más fácil imprimir un informe y mantener constantemente las hojas necesarias frente a sus ojos, pero luego la tabla en la pantalla simplemente pierde su significado.

En general, los bloques pegados no se pueden evitar. Y SSRS 2014 no sabe cómo arreglar filas y columnas en un documento MHTML, solo en su propia interfaz web.

Aquí recordamos que los navegadores modernos admiten la propiedad adhesiva CSS , que solo proporciona la función que necesitamos. Ponemos posición: pegajoso en el bloque marcado, especificamos la sangría a la izquierda o en la parte superior (izquierda, propiedades superiores), y el bloque permanecerá en su lugar durante el desplazamiento horizontal y vertical.

Necesita encontrar un parámetro que CSS pueda alcanzar. Los valores de celda personalizados que permiten que SSRS 2014 los capture en la interfaz web se pierden al exportar a HTML. Bien, los marcaremos nosotros mismos, solo entenderíamos cómo.

Después de varias horas de lectura de documentación y discusiones con colegas, parecía que no había opciones. Y aquí, según todas las leyes de la trama, el campo de información sobre herramientas apareció para nosotros, lo que nos permite especificar información sobre herramientas para las celdas. Resultó que se arroja al código HTML exportado en el atributo de información sobre herramientas, exactamente en la etiqueta que pertenece a la celda personalizada en las Herramientas de datos de SQL Server. No había otra opción: no encontramos otra forma de marcar las células para su fijación.

Por lo tanto, debe crear reglas de marcado y reenviar marcadores en HTML a través de la información sobre herramientas. Luego, usando JS, cambiamos el atributo de información sobre herramientas a la clase CSS en el marcador especificado.

Solo hay dos formas de arreglar las celdas: verticalmente (columna fija) y horizontalmente (fila fija). Tiene sentido colocar otro marcador en las celdas de la esquina, que permanecen en su lugar cuando se desplaza en ambas direcciones, fijo-ambos.

El siguiente paso es hacer la interfaz de usuario. Cuando recibe un documento HTML, necesita encontrar todos los elementos HTML con marcadores en él, reconocer los valores, establecer la clase CSS adecuada y eliminar el atributo de información sobre herramientas para que no salga al pasar el mouse sobre él. Cabe señalar que el marcado resultante consiste en tablas anidadas (etiquetas de tabla).

Ver código
type FixationType = 'row' | 'column' | 'both';

init(reportHTML: HTMLElement) {
    //    

    // -  
    const rowsFixed: NodeList = reportHTML.querySelectorAll('[title^="RowFixed"]');
    // -  
    const columnFixed: NodeList = reportHTML.querySelectorAll('[title^="ColumnFixed"]');
    // -    
    const bothFixed: NodeList = reportHTML.querySelectorAll('[title^="BothFixed"]');

    this.prepare(rowsFixed, 'row');
    this.prepare(columnFixed, 'column');
    this.prepare(bothFixed, 'both');
}

//    
prepare(nodeList: NodeList, fixingType: FixationType) {
    for (let i = 0; i < nodeList.length; i++) {
        const element: HTMLElement = nodeList[i];
        //   -
        element.classList.add(fixingType + '-fixed');

        element.removeAttribute('title');
        element.removeAttribute('alt'); //   SSRS

        element.parentElement.classList.add(fixingType  + '-fixed-parent');

        //     ,     
        element.style.width = element.getBoundingClientRect().width  + 'px';
        //     ,     
        element.style.height = element.getBoundingClientRect().height  + 'px';

        //  
        this.calculateCellCascadeParams(element, fixingType);
    }
}


Y aquí hay un nuevo problema: con el comportamiento en cascada, cuando varios bloques que se mueven en una dirección se fijan a la vez en la tabla, las celdas que van una tras otra se colocarán en capas. Al mismo tiempo, no está claro cuánto debe retirarse cada bloque siguiente: las sangrías deberán calcularse a través de JavaScript en función de la altura del bloque que se encuentra frente a él. Todo esto se aplica a los anclajes verticales y horizontales.

El script de corrección resolvió el problema.
//      
calculateCellCascadeParams(cell: HTMLElement, fixationType: FixationType) {
    const currentTD: HTMLTableCellElement = cell.parentElement;
    const currentCellIndex = currentTD.cellIndex;

    //   
    currentTD.style.left = '';
    currentTD.style.top = '';

    const currentTDStyles = getComputedStyle(currentTD);

    //  
    if (fixationType === 'row' || fixationType === 'both') {
        const parentRow: HTMLTableRowElement = currentTD.parentElement;

        //        
        //    .
        //   ,    .
        let previousRow: HTMLTableRowElement = parentRow;
        let topOffset = 0;

        while (previousRow = previousRow.previousElementSibling) {
            let previousCellIndex = 0;
            let cellIndexBulk = 0;

            for (let i = 0; i < previousRow.cells.length; i++) {
                if (previousRow.cells[i].colSpan > 1) {
                    cellIndexBulk += previousRow.cells[i].colSpan;
                } else {
                    cellIndexBulk += 1;
                }

                if ((cellIndexBulk - 1) >= currentCellIndex) {
                    previousCellIndex = i;
                    break;
                }
            }

            const previousCell = previousRow.cells[previousCellIndex];

            if (previousCell.classList.contains(fixationType + '_fixed_parent')) {
                topOffset += previousCell.getBoundingClientRect().height;
            }
        }

        if (topOffset > 0) {
            if (currentTDStyles.top) {
                topOffset += <any>currentTDStyles.top.replace('px', '') - 0;
            }

            currentTD.style.top = topOffset + 'px';
        }
    }

    //  
    if (fixationType === 'column' || fixationType === 'both') {
        //       
        //     .
        //   ,    .
        let previousCell: HTMLTableCellElement = currentTD;
        let leftOffset = 0;

        while (previousCell = previousCell.previousElementSibling) {
            if (previousCell.classList.contains(fixationType + '_fixed_parent')) {
                leftOffset += previousCell.getBoundingClientRect().width;
            }
        }

        if (leftOffset > 0) {
            if (currentTDStyles.left) {
                leftOffset += <any>currentTDStyles.left.replace('px', '') - 0;
            }

            currentTD.style.left = leftOffset + 'px';
        }
    }
}


El código verifica las etiquetas de los elementos marcados y agrega los parámetros de las celdas fijas al valor de sangría. En el caso de las filas adherentes, su altura se suma, para las columnas, su ancho.


Un ejemplo de un informe con una línea superior adhesiva.

Como resultado, el proceso se ve así:

  1. Obtenemos el marcado de SSRS y lo pegamos en el lugar correcto en el DOM;
  2. Reconocer marcadores;
  3. Ajuste los parámetros para el comportamiento en cascada.

Dado que el comportamiento de bloqueo se implementa completamente a través de CSS, y JS solo participa en la preparación del documento entrante, la solución funciona lo suficientemente rápido y sin retrasos.

Desafortunadamente, para IE, los bloques pegados tuvieron que desactivarse porque no es compatible con la posición: propiedad adhesiva. El resto, Safari, Mozilla Firefox y Chrome, hacen un excelente trabajo.

Siga adelante.

Problema número 2. Informe de exportación


Para extraer un informe del sistema, debe (1) acceder al SSRS a través de ReportService para un objeto Blob, (2) obtener un enlace al objeto a través de la interfaz utilizando el método window.URL.createObjectURL, (3) colocar el enlace en la etiqueta y simular un clic para Subir archivo.

Esto funciona en Firefox, Safari y en todas las versiones de Chrome, excepto Apple. Para que IE, Edge y Chrome para iOS también soportaran la función, tuve que retroceder.

En IE y Edge, el evento simplemente no activará una solicitud del navegador para descargar el archivo. Estos navegadores tienen una función tal que para simular un clic, se requiere la confirmación del usuario para descargar, así como una clara indicación de acciones adicionales. La solución se encontró en el método window.navigator.msSaveOrOpenBlob (), que está disponible tanto en IE como en Edge. Simplemente sabe cómo pedirle permiso al usuario para la operación y aclarar qué hacer a continuación. Entonces, determinamos si existe el método window.navigator.msSaveOrOpenBlob y actuamos en función de la situación.

Chrome en iOS no tenía ese truco y, en lugar de un informe, obtuvimos solo una página en blanco. Deambulando por la Web, encontramos una historia similar, juzgando por qué en iOS 13 este error debería haberse solucionado. Desafortunadamente, escribimos la aplicación en los días de iOS 12, por lo que al final decidimos no perder más tiempo y simplemente apagamos el botón en Chrome para iOS.
Ahora sobre cómo se ve el proceso de exportación final a la interfaz de usuario. Hay un botón en el componente de informe angular que inicia una cadena de pasos:

  • a través de los parámetros del evento, el controlador recibe el identificador del formato de exportación (por ejemplo, "PDF");
  • Envía una solicitud a ReportService para recibir un objeto Blob para el formato especificado;
  • comprueba si el navegador es IE o Edge;
  • cuando la respuesta proviene de ReportService:
    • si es IE o Edge, llama a window.navigator.msSaveOrOpenBlob (fileStream, fileName);
    • de lo contrario, llama al método this.exportDownload (fileStream, fileName), donde fileStream es el Blob obtenido de la solicitud a ReportService, y fileName es el nombre del archivo a guardar. El método crea una etiqueta oculta con un enlace a window.URL.createObjectURL (fileStream), simula un clic y elimina la etiqueta.

Con esto resuelto, la última aventura permaneció.

Problema número 3. Imprimir


Ahora podemos ver el informe en el portal y exportarlo a formatos XLS, PDF, DOCX. Queda por implementar la impresión del documento para obtener un informe preciso de varias páginas. Si la tabla resultó estar dividida en páginas, cada una de ellas debería contener encabezados, los mismos bloques adhesivos de los que hablamos en la sección anterior.

La opción más fácil es tomar la página actual con el informe mostrado, ocultar todo lo superfluo usando CSS y enviarlo a imprimir usando el método window.print (). Este método no funciona de inmediato por varias razones:

  1. Área de visualización no estándar: el informe en sí está contenido en un área desplazable por separado para que la página no se extienda a dimensiones horizontales increíbles. El uso de window.print () recorta el contenido que no se ajusta a la pantalla;
  2. , ;
  3. , .

Todo esto se puede solucionar con JS y CSS, pero decidimos ahorrar tiempo a los desarrolladores y buscar una alternativa a window.print ().

SSRS puede darnos de inmediato un PDF listo para usar con una paginación presentable. Esto nos salva de todas las dificultades de la versión anterior, la única pregunta es, ¿podemos imprimir el PDF a través de un navegador?

Debido a que PDF es un estándar de terceros, los navegadores lo admiten a través de varios complementos de visor. Sin plug-in, sin dibujos animados, por lo que nuevamente necesitamos una opción alternativa.

¿Y si coloca el PDF en la página como una imagen y envía esta página para imprimir? Ya hay bibliotecas y componentes para Angular que proporcionan esa representación. Buscado, experimentado, implementado.

Para no tratar con los datos que no queremos imprimir, se decidió transferir el contenido representado a una nueva página, y allí ya se ejecuta window.print (). Como resultado, todo el proceso es el siguiente:

  1. Solicite ReportService para exportar el informe en formato PDF;
  2. Obtenemos el objeto Blob, lo convertimos en una URL (URL.createObjectURL (fileStream)), le damos la URL al visor de PDF para su representación;
  3. Tomamos imágenes del visor de PDF;
  4. Abra una nueva página y agregue un pequeño marcado allí (título, una pequeña sangría);
  5. Agregue la imagen del visor de PDF al marcado, llame a window.print ().

Después de varias verificaciones, también apareció un código JS en la página, que, antes de imprimir, verifica que todas las imágenes se hayan cargado.

Por lo tanto, la apariencia completa del documento está determinada por los parámetros de la plantilla SSRS, y la interfaz de usuario no interfiere con este proceso. Esto reduce la cantidad de posibles errores. Dado que las imágenes se transfieren para su impresión, estamos asegurados contra cualquier daño o deformación del diseño.

También hay desventajas:

  • un gran informe pesará mucho, lo que afectará negativamente a las plataformas móviles;
  • el diseño no se actualiza automáticamente: los colores, las fuentes y otros elementos de diseño deben instalarse a nivel de plantilla.

En nuestro caso, no se esperaba la adición frecuente de nuevas plantillas, por lo que la solución era aceptable. El rendimiento móvil se ha dado por sentado.

La última palabra


Así es como un proyecto normal nos hace buscar soluciones simples para tareas no triviales. El producto final cumple totalmente con los requisitos de diseño y se ve hermoso. Y lo más importante, aunque no tuvimos que buscar los métodos de implementación más obvios, la tarea se completó más rápido que si asumiéramos el módulo de informe original con todas las consecuencias. Y al final, pudimos centrarnos en los objetivos comerciales del proyecto.

Source: https://habr.com/ru/post/undefined/


All Articles