DEV Community

loading...
Cover image for Celery progress bar in React

Celery progress bar in React

Tek Bahadur Kshetri
I am a Geomatics engineer working as a research associate in Geoinformatics Center (GIC), AIT, Thailand. I am mainly passionated for spatial data analysis and web-GIS development.
・4 min read

1. Overview
2. Backend setup
3. Frontend setup

1. Overview

The progress bar is one of the most useful UI components for tracking the actual progress of the task. But It is still a complex task to track the exact progress. In this tutorial, I will guide you to make the progress bar using celery-progress library with react.

Please check Celery Progress Bars for Django before starting this tutorial.

In this tutorial, I am going to upload the data to the server using Django Rest Framework (DRF), preprocess the data in the server, and send back the response. In my case, the preprocessing data will take 2-3 min, that's why I will visualize the progress bar of the processing in the frontend using react.

2. Backend setup

I consider you already set up your Django with celery. Now you need to install the celery-progress library using pip,

pip install celery-progress
Enter fullscreen mode Exit fullscreen mode

Add the endpoint URL to the celery-progress in urls.py file,

from django.urls import re_path, include
re_path(r'^celery-progress/', include('celery_progress.urls')),
Enter fullscreen mode Exit fullscreen mode

I am just writing the example function as celery_function. You need to replace the actual progress tracker function.

from celery import shared_task
from celery_progress.backend import ProgressRecorder
import time

@shared_task(bind=True)
def celery_function(self, seconds):
    progress_recorder = ProgressRecorder(self)
    result = 0
    for i in range(seconds):
        time.sleep(1)
        result += i
        progress_recorder.set_progress(i + 1, seconds)
    return result
Enter fullscreen mode Exit fullscreen mode

I have a Data model like following,

class Data(models.Model):
    name = models.CharField(max_length=100)
    taskId = models.CharField(max_length=200, blank=True)
    ...
    ...
    ...
Enter fullscreen mode Exit fullscreen mode

Now, lets overwrite the create method in your ViewSet class of DRF and call the celery_function.delay(time) function.

In my code, my model name is Data, the serializer class is DataSerializer. I used the patch method to append the task_id to the Data model.

class DataViewSet(viewsets.ModelViewSet):
    queryset = Data.objects.all()
    serializer_class = DataSerializer
    permission_classes = [permissions.IsAuthenticated]

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)

        if serializer.is_valid():
            data = serializer.data
            id = data['id']

            task = celery_function.delay(10)

            self.patch(id, {"taskId": task.id})

        return Response({'task_id': task.id}, status=status.HTTP_201_CREATED)

    def patch(self, pk, taskid):
        instance = self.get_object(pk)
        serializer = DataSerializer(
            instance, data=taskid, partial=True)

        if serializer.is_valid():
            serializer.save()
Enter fullscreen mode Exit fullscreen mode

We set up everything in the backend. The task progress will be available on this URL: http://localhost:8000/celery-progress/${task_id}.

3. Frontend Setup

In the frontend, I am going to write the custom progress bar using react-bootstrap. The DRF backend will provide us the task progress in this URL: http://localhost:8000/celery-progress/${task_id}. Now we need to recursively hit this URL until the task status changes to complete. For this let's fetch the progress using axios.

For this tutorial, I am using react-redux. In the redux actions.js file, I have created two functions, one for adding the data (named as addData), and another for getting the progress of the task (named as getProgress). The addData function will only useful for adding the data to the server.

export const addData = (data) => (dispatch, getState) => {
  axios
    .post(`http://localhost:8000/api/data/`, data)
    .then((res) => {
      dispatch({
        type: ADD_DATA,
        payload: res.data,
      });

      const task_id = res.data?.task_id;
      dispatch(getProgress(task_id));
    })
    .catch((err) =>
      console.log(err)
    );
};
Enter fullscreen mode Exit fullscreen mode

The getProgress will take the actual progress of the task in with a JSON response. The example of the json response is as below,

{"complete": true, "success": true, "progress": {"pending": false, "current": 100, "total": 100, "percent": 100}, "result": "Done"}
Enter fullscreen mode Exit fullscreen mode

Here, I recursively call the getProgress function to get the current progress until the task gets completed.

export const getProgress = (taskId) => {
  return (dispatch) => {
    return axios
      .get(`http://localhost:8000/celery-progress/${taskId}`)
      .then((res) => {
        dispatch({
          type: PROGRESS_BAR,
          payload: { taskid: taskId, ...res.data },
        });
        if (!res.data.complete && !res.data?.progess?.pending) {
          return dispatch(getProgress(taskId));
        }
      })
      .catch((err) =>
        console.log(err)
      );
  };
};
Enter fullscreen mode Exit fullscreen mode

Also in the redux reducers.js file, I added the response as follows,

import { ADD_DATA, PROGRESS_BAR } from "../actions/types";

const initialState = {
  progress: {},
  data: [],
};

export default function (state = initialState, action) {
  switch (action.type) {
    case PROGRESS_BAR:
      return {
        ...state,
        progress: action.payload,
      };

    case ADD_DATA:
      return {
        ...state,
        data: action.payload,
      };
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's write the react component to visualize the progress bar, as below,

import React, { Component } from "react";
import { connect } from "react-redux";
import { ProgressBar } from "react-bootstrap";

class ProgressBarUi extends Component {
  render() {
    const { percent } = this.props;
    return (
      <ProgressBar
        now={percent}
        animated
        variant="success"
        label={`${percent}%`}
      />
    );
  }
}

export default ProgressBarUi;
Enter fullscreen mode Exit fullscreen mode

The progress bar needs to visualize only when the progress is not completed, and not pending,

import React, { Component } from "react";
import { connect } from "react-redux";
import { addData } from "../../../actions";
import ProgressBarUi from "../../common/ProgressBar";

class AddData extends Component {
  onSubmit = (e) => {
    const data = {
      key1: "value1",
      key2: "value2",
    };
    this.props.addExposure(data);
  };

  render() {
    const { progress } = this.props;

    return (
      <div>
        {/* your progress bar goes here  */}
        {progress?.progress && !progress?.complete && (
          <ProgressBarUi percent={progress.progress?.percent} />
        )}

        ...
        ...
        ...
        {/* data submit button */}
        <input type="submit" value="Add data" onSubmit={this.onSubmit} />
      </div>
    );
  }
}

const mapStateToProps = (state) => ({
  progress: state.progress,
});

export default connect(mapStateToProps, {
  addData,
})(AddData);
Enter fullscreen mode Exit fullscreen mode

Alright, finally you successfully set up the progress bar using Django, react, and celery.

Discussion (5)

Collapse
allaye profile image
Allaye

Hey Tek thanks for the piece here, i want to ask, if i am uploading an image how do i get the size of the image or file uploaded?

Collapse
iamtekson profile image
Tek Bahadur Kshetri Author

For getting the size of the image, you can write the following piece of code to get the file size,

fileChangedHandler = (event) => {
    let file_size = event.target.files[0].size;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
allaye profile image
Allaye

Hi thanks for your reply, but am corrently looking for how to implement this in a django backend

Collapse
vikramiiitm profile image
Vikram Choudhary

is there code in github?

Collapse
iamtekson profile image
Tek Bahadur Kshetri Author

Sorry, The code is part of a private project. I can't share.