Clarity Server-Driven DataGrid

Overview

Clarity provides Server-Driven DataGrid. Using Query DSL we will fetch page by page data and render it in clarity server-driven data grid

Github: https://github.com/gitorko/project86

Quick Overview

To deploy the application in a single command, clone the project, make sure no conflicting docker containers or ports are running and then run

1git clone https://github.com/gitorko/project86
2cd project86
3docker-compose -f docker/docker-compose.yml up 

Open http://localhost:8080/

Server-Driven DataGrid

When dealing with large amounts of data or heavy processing, a DataGrid often has to access the currently displayed data only, requesting only the necessary pieces of data from the server.

Implementation

Design

Code

 1package com.demo.project86.controller;
 2
 3import com.demo.project86.domain.Customer;
 4import com.demo.project86.domain.CustomerBinderCustomizer;
 5import com.demo.project86.repo.CustomerRepository;
 6import lombok.RequiredArgsConstructor;
 7import lombok.extern.slf4j.Slf4j;
 8import org.springframework.beans.factory.annotation.Autowired;
 9import org.springframework.data.domain.Page;
10import org.springframework.data.domain.Pageable;
11import org.springframework.data.querydsl.binding.QuerydslPredicate;
12import org.springframework.data.web.PageableDefault;
13import org.springframework.web.bind.annotation.DeleteMapping;
14import org.springframework.web.bind.annotation.GetMapping;
15import org.springframework.web.bind.annotation.PathVariable;
16import org.springframework.web.bind.annotation.PostMapping;
17import org.springframework.web.bind.annotation.RequestBody;
18import org.springframework.web.bind.annotation.RestController;
19
20@RestController
21@Slf4j
22@RequiredArgsConstructor
23public class HomeController {
24
25    @Autowired
26    CustomerRepository customerRepo;
27
28    @GetMapping(value = "/api/customer")
29    public Page<Customer> getCustomers(@PageableDefault(size = 20) Pageable pageRequest,
30                                       @QuerydslPredicate(root = Customer.class, bindings = CustomerBinderCustomizer.class) com.querydsl.core.types.Predicate predicate) {
31        return customerRepo.findAll(predicate, pageRequest);
32    }
33
34    @PostMapping(value = "/api/customer")
35    public Customer saveCustomer(@RequestBody Customer customer) {
36        log.info("Saving customer!");
37        return customerRepo.save(customer);
38    }
39
40    @DeleteMapping(value = "/api/customer/{id}")
41    public void deleteCustomer(@PathVariable Long id) {
42        log.info("Deleting customer: {}", id);
43        customerRepo.deleteById(id);
44    }
45
46}
 1package com.demo.project86.domain;
 2
 3import java.io.Serializable;
 4import jakarta.persistence.Basic;
 5import jakarta.persistence.Column;
 6import jakarta.persistence.Entity;
 7import jakarta.persistence.GeneratedValue;
 8import jakarta.persistence.GenerationType;
 9import jakarta.persistence.Id;
10import jakarta.persistence.Table;
11import jakarta.validation.constraints.Size;
12
13import lombok.AllArgsConstructor;
14import lombok.Builder;
15import lombok.Data;
16import lombok.NoArgsConstructor;
17
18@Entity
19@Table(name = "customer")
20@Data
21@Builder
22@AllArgsConstructor
23@NoArgsConstructor
24public class Customer implements Serializable {
25
26    private static final long serialVersionUID = 1L;
27    @Id
28    @GeneratedValue(strategy = GenerationType.AUTO)
29    @Basic(optional = false)
30    @Column(name = "id")
31    private Long id;
32    @Size(max = 45)
33    @Column(name = "first_name")
34    private String firstName;
35    @Size(max = 45)
36    @Column(name = "last_name")
37    private String lastName;
38    private String city;
39
40}
 1package com.demo.project86.domain;
 2
 3import java.util.Collection;
 4import java.util.Optional;
 5
 6import com.querydsl.core.BooleanBuilder;
 7import com.querydsl.core.types.Predicate;
 8import com.querydsl.core.types.dsl.StringPath;
 9import org.springframework.data.querydsl.binding.MultiValueBinding;
10import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer;
11import org.springframework.data.querydsl.binding.QuerydslBindings;
12
13public class CustomerBinderCustomizer implements QuerydslBinderCustomizer<QCustomer> {
14
15    @Override
16    public void customize(QuerydslBindings querydslBindings, QCustomer qCustomer) {
17        querydslBindings.including(
18                qCustomer.id,
19                qCustomer.firstName,
20                qCustomer.lastName,
21                qCustomer.city
22        );
23
24        StringPath[] multiPropertySearchPaths = new StringPath[]{qCustomer.firstName, qCustomer.lastName, qCustomer.city};
25
26        querydslBindings.bind(multiPropertySearchPaths).all(new MultiValueBinding<>() {
27            @Override
28            public Optional<Predicate> bind(StringPath path, Collection<? extends String> values) {
29                BooleanBuilder predicate = new BooleanBuilder();
30                values.forEach(value -> predicate.or(path.containsIgnoreCase(value)));
31                return Optional.of(predicate);
32            }
33        });
34
35    }
36}
1package com.demo.project86.repo;
2
3import com.demo.project86.domain.Customer;
4import org.springframework.data.jpa.repository.JpaRepository;
5import org.springframework.data.querydsl.QuerydslPredicateExecutor;
6
7public interface CustomerRepository extends JpaRepository<Customer, Long>, QuerydslPredicateExecutor<Customer> {
8}
 1<div class="content-container">
 2  <div class="content-area">
 3    <div class="alert-section">
 4      <app-alert></app-alert>
 5    </div>
 6
 7    <div class="clr-row">
 8      <div class="clr-col-12">
 9        <h2 style="text-align: center">Customers</h2>
10        <clr-datagrid [clrDgLoading]="loading" (clrDgRefresh)="refresh($event)">
11          <clr-dg-placeholder class="content-center">No Customers!</clr-dg-placeholder>
12          <clr-dg-column [clrDgField]="'id'">ID</clr-dg-column>
13          <clr-dg-column [clrDgField]="'firstName'">
14            <ng-container *clrDgHideableColumn="{hidden: false}">First Name</ng-container>
15          </clr-dg-column>
16          <clr-dg-column [clrDgField]="'lastName'">
17            <ng-container *clrDgHideableColumn="{hidden: false}">Last Name</ng-container>
18          </clr-dg-column>
19          <clr-dg-column [clrDgField]="'city'">
20            <ng-container *clrDgHideableColumn="{hidden: false}">City</ng-container>
21            <clr-dg-filter [clrDgFilter]="cityFilter">
22              <app-checkbox-filter
23                #cityFilter
24                [filterValues]="cityFilterValues"
25                filterKey="city"></app-checkbox-filter>
26            </clr-dg-filter>
27          </clr-dg-column>
28          <clr-dg-column>Action</clr-dg-column>
29          <!-- structural directive -->
30          <clr-dg-row *ngFor="let customer of customerPage?.content">
31            <clr-dg-cell>{{customer.id}}</clr-dg-cell>
32            <clr-dg-cell>{{customer.firstName}}</clr-dg-cell>
33            <clr-dg-cell>{{customer.lastName}}</clr-dg-cell>
34            <clr-dg-cell>{{customer.city}}</clr-dg-cell>
35            <clr-dg-cell>
36              <cds-icon shape="trash" style="cursor: pointer; color: blue" (click)="deleteCustomer(customer)">
37              </cds-icon>
38            </clr-dg-cell>
39          </clr-dg-row>
40
41          <clr-dg-footer>
42            <clr-dg-pagination #pagination [clrDgPageSize]="10" [(clrDgPage)]="page"
43                               [clrDgTotalItems]="total">
44              <clr-dg-page-size [clrPageSizeOptions]="[10,20,50,100]">Customers per page</clr-dg-page-size>
45              {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} of {{pagination.totalItems}} customers
46            </clr-dg-pagination>
47          </clr-dg-footer>
48
49        </clr-datagrid>
50      </div>
51    </div>
52
53    <div class="clr-row">
54      <div class="clr-col-12">
55        <form class="clr-form clr-form-horizontal">
56          <div class="clr-form-control">
57            <label for="firstName" class="clr-control-label">First Name</label>
58            <div class="clr-control-container">
59              <div class="clr-input-wrapper">
60                <!-- two way data binding -->
61                <input type="text" [(ngModel)]="customer.firstName" id="firstName" name="firstName"
62                       placeholder="Placeholder" class="clr-input"/>
63              </div>
64            </div>
65          </div>
66          <div class="clr-form-control">
67            <label for="lastName" class="clr-control-label">Last Name</label>
68            <div class="clr-control-container">
69              <div class="clr-input-wrapper">
70                <input [(ngModel)]="customer.lastName" type="text" id="lastName" name="lastName"
71                       placeholder="Placeholder" class="clr-input"/>
72              </div>
73            </div>
74          </div>
75          <div class="clr-form-control">
76            <div class="clr-control-container">
77              <!-- event binding -->
78              <button type="submit" class="btn btn-primary" (click)="saveCustomer()">Save</button>
79            </div>
80          </div>
81        </form>
82      </div>
83    </div>
84
85  </div>
86</div>

The debounceTime added to debounce the events so that rest api doesn't get called for every keystroke.

 1import {Component, OnInit, ViewChild} from '@angular/core';
 2import {Customer} from '../../models/customer';
 3import {RestService} from '../../services/rest.service';
 4import {Router} from '@angular/router';
 5import {ClarityIcons, trashIcon} from '@cds/core/icon';
 6import {ClrDatagridStateInterface} from '@clr/angular';
 7import {CustomerPage} from "../../models/customer-page";
 8import {AlertComponent} from "../alert/alert.component";
 9import {Subject} from "rxjs";
10import {debounceTime} from "rxjs/operators";
11
12@Component({
13  selector: 'app-home',
14  templateUrl: './home.component.html',
15  styleUrls: []
16})
17export class HomeComponent implements OnInit {
18
19  customerPage: CustomerPage = new CustomerPage();
20  customer: Customer = new Customer();
21  loading = false;
22  page: number = 1;
23  total: number = 1;
24  cityFilterValues: string[] = [];
25  tableState: ClrDatagridStateInterface = {page: {current: 1, from: 1, size: 10, to: 10}};
26  debouncer: Subject<any> = new Subject<any>();
27
28  // @ts-ignore
29  @ViewChild(AlertComponent, {static: true}) private alert: AlertComponent;
30
31  constructor(private restService: RestService, private router: Router) {
32    ClarityIcons.addIcons(trashIcon);
33    this.cityFilterValues.push("Bangalore");
34    this.cityFilterValues.push("New York");
35    this.cityFilterValues.push("London");
36  }
37
38  ngOnInit(): void {
39    this.loading = true;
40    this.debouncer
41      .pipe(debounceTime(700))
42      .subscribe(state => {
43        this.tableState = state;
44        this.loading = true;
45        if (!state.page) {
46          state.page = {
47            from: 1,
48            to: 10,
49            size: 10,
50          };
51        }
52        // @ts-ignore
53        let pageStart = state.page.current - 1;
54        let pageSize = state.page.size;
55        this.restService.getCustomers(pageStart, pageSize, state.filters, state.sort).subscribe(data => {
56            this.customerPage = data;
57            this.total = this.customerPage?.totalElements;
58            this.loading = false;
59          },
60          error => {
61            this.loading = false;
62          });
63        }
64      );
65  }
66
67  saveCustomer(): void {
68    console.log('save customer!');
69    this.restService.saveCustomer(this.customer)
70      .subscribe(data => {
71        this.alert.showSuccess('Saved customer: ' + this.customer.firstName);
72        this.refresh(this.tableState);
73      });
74  }
75
76  deleteCustomer(customer: Customer): void {
77    console.log('deleting customer : ' + customer.id);
78    this.restService.deleteCustomer(customer.id)
79      .subscribe(data => {
80        this.alert.showSuccess('Deleted customer: ' + customer.id);
81        this.refresh(this.tableState);
82      });
83  }
84
85  refresh(state: ClrDatagridStateInterface) {
86    this.debouncer.next(state);
87  }
88
89}

Setup

 1# Project 86
 2
 3Clarity - Server Driven Data Grid with QueryDSL
 4
 5[https://gitorko.github.io/clarity-server-driven-datagrid/](https://gitorko.github.io/clarity-server-driven-datagrid/)
 6
 7### Version
 8
 9Check version
10
11```bash
12$java --version
13openjdk version "21.0.3" 2024-04-16 LTS
14
15node --version
16v16.16.0
17
18yarn --version
191.22.18
20```
21
22### Postgres DB
23
24```
25docker run -p 5432:5432 --name pg-container -e POSTGRES_PASSWORD=password -d postgres:9.6.10
26docker ps
27docker exec -it pg-container psql -U postgres -W postgres
28CREATE USER test WITH PASSWORD 'test@123';
29CREATE DATABASE "test-db" WITH OWNER "test" ENCODING UTF8 TEMPLATE template0;
30grant all PRIVILEGES ON DATABASE "test-db" to test;
31
32docker stop pg-container
33docker start pg-container
34```
35
36### Dev
37
38To Run backend in dev mode
39
40```bash
41cd project86
42./gradlew bootRun
43```
44
45To Run UI in dev mode
46
47```bash
48cd ui
49yarn install
50yarn build
51yarn start
52```
53
54Open [http://localhost:4200/](http://localhost:4200/)
55
56### Prod
57
58To run as a single jar, both UI and backend are bundled to single uber jar.
59
60```bash
61./gradlew cleanBuild
62cd project86/build/libs
63java -jar project86-1.0.0.jar
64```
65
66Open [http://localhost:8080/](http://localhost:8080/)
67
68### Docker
69
70```bash
71./gradlew cleanBuild
72docker build -f docker/Dockerfile --force-rm -t project86:1.0.0 .
73docker images |grep project86
74docker tag project86:1.0.0 gitorko/project86:1.0.0
75docker push gitorko/project86:1.0.0
76docker-compose -f docker/docker-compose.yml up 
77```

References

https://clarity.design/

https://clarity.design/angular-components/datagrid/#server-driven-datagrid

comments powered by Disqus