1074

Progress Bars in Trongate

Showing 1 to 10 of 12 comments.

Comments for “Progress Bars in Trongate”
 

Posted by Dom on Thursday 18th April 2024 at 10:05 GMT

I'm trying to implement a progress bar but first and foremost I'd like to get a simple one working before trying to adapt it to what I really want it to do.

I created a simple module called progress and added the a progress.php view

Progress Bar Example


Progress Bar Example



 function updateProgressBar(progress) {
 // Convert progress to integer (if it's not already)
 var progressInt = parseInt(progress);

 // Update progress bar
 document.getElementById('progressBar').value = progressInt;
 // Log current progress value to the console
 console.log('Progress value:', progressInt);
 }

 // Fetch the progress updates from the controller
 fetch('progress/updateProgressBar')
 .then(response => {
 if (!response.ok) {
 throw new Error('Network response was not ok');
 }
 return response.text();
 })
 .then(data => {
 eval(data);
 })
 .catch(error => console.error('Error fetching progress:', error));


and a corresponding Progress.php controller

                            
Level One Member

Dom

User Level: Level One Member

Date Joined: 12/01/2024

Posted by djnordeen on Thursday 18th April 2024 at 10:33 GMT

It seems to me that there is a progress bar somewhere in one or more of Trongate's .js files.
Dan
Early Adopter

djnordeen

User Level: Early Adopter

Date Joined: 20/08/2021

Posted by Dom on Thursday 18th April 2024 at 11:27 GMT

It's interesting that you should say that Dan because I had been wondering the same, yet despite repeated searches I couldn't find anything. Trongate_Pages has something related to progress but I can't see that coming into play with this.

It could of course be a security thing. Need to learn more about that.
Level One Member

Dom

User Level: Level One Member

Date Joined: 12/01/2024

Posted by Dom on Thursday 18th April 2024 at 15:34 GMT

Well I made some progress on this, pardon the pun,

view code is now
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Progress Bar Example</title>
</head>
<body>
<h1>Progress Bar Example</h1>
<!--<progress id="vtlProg" max="100" value="10"></progress>-->
<div class="container">
    <div class="row">
        <div class="col-md-12">
            <p>&nbsp;</p>
            <p>&nbsp;</p>
            <button type="button" id="button1" onclick="submit1()">Click Here To Start Progress Bar</button>
            <p>&nbsp;
            <p>
                <button type="button" id="button2" onclick="submit2()">Click Here To Stop Progress Bar</button>
        </div>
        <div class="col-md-12">
            <p>&nbsp;</p>
            <p>&nbsp;</p>
            <div id="progressbar" style="border:1px solid #ccc; border-radius: 5px; "></div>

            <!-- Progress information -->
            
            <div id="information"></div>
        </div>
    </div>
</div>

<iframe id="loadarea" style="display:none;"></iframe>
<br/>
<script>

    function submit1() {
        document.getElementById('loadarea').src = '<?=BASE_URL ?>progress/update';
    }

    function submit2() {
        document.getElementById('loadarea').src = '';
    }


    //$("#button1").click(function () {
    //    document.getElementById('loadarea').src = '<?php //=BASE_URL ?>//progress/update';
    //});
    //$("#button2").click(function () {
    //    document.getElementById('loadarea').src = '';
    //});

    //function updateProgressBar(progress) {
    //    // Convert progress to integer (if it's not already)
    //    var progressInt = parseInt(progress);
    //
    //    // Update progress bar
    //    document.getElementById('vtlProg').value = progressInt;
    //    // Log current progress value to the console
    //    console.log('Progress value:', progressInt);
    //}
    //
    //// Fetch the progress updates from the controller
    //fetch('<?php //= BASE_URL ?>//progress/updateProgressBar')
    //
    //    .then(response => {
    //        console.log('response = ', response);
    //        if (!response.ok) {
    //            throw new Error('Network response was not ok');
    //        }
    //        return response.text();
    //    })
    //    .then(data => {
    //        console.log(data);
    //        eval(data);
    //
    //    })
    //    .catch(error => console.error('Error fetching progress:', error));
</script>
</body>
</html>


and controller is now this
<?php

class Progress extends Trongate
{
    public function index(): void
    {
        $data['view_module'] = 'progress'; // Indicates the module where the view file exists.
        $data['view_file'] = 'progress'; // Specifies the base name of the target PHP view file.
        $this->template('public', $data); // Loads the 'progress' view file within the public template.
    }

    public function update()
    {
        session_start();

        ini_set('max_execution_time', 0); // to get unlimited php script execution time

        if (empty($_SESSION['i'])) {
            $_SESSION['i'] = 0;
        }

        $total = 100;
        for ($i = $_SESSION['i']; $i < $total; $i++) {
            $_SESSION['i'] = $i;
            $percent = intval($i / $total * 100) . "%";

            sleep(1); // Here call your time taking function like sending bulk sms etc.

            echo '<script>
    parent.document.getElementById("progressbar").innerHTML="<div style=\"width:' . $percent . ';background:linear-gradient(to bottom, rgba(125,126,125,1) 0%,rgba(14,14,14,1) 100%); ;height:35px;\">&nbsp;</div>";
    parent.document.getElementById("information").innerHTML="<div style=\"text-align:center; font-weight:bold\">' . $percent . ' is processed.</div>";</script>';

            ob_flush();
            flush();
        }
        echo '<script>parent.document.getElementById("information").innerHTML="<div style=\"text-align:center; font-weight:bold\">Process completed</div>"</script>';

        session_destroy();
    }

    public function updateProgressBar()
    {

        for ($i = 0; $i <= 100; $i += 10) {
            usleep(500000);
            echo "<script>updateProgressBar($i);</script>";
            ob_flush();
            flush();
        }
    }
}


Crucially I had to change the .htaccess so that it now looks like this
<IfModule mod_headers.c>
#    Header set Content-Security-Policy "frame-ancestors 'none'"
Header set Content-Security-Policy "frame-ancestors 'self' http://localhost:*;"
</IfModule>

<IfModule mod_rewrite.c>
  RewriteEngine on
  RewriteRule ^$ public/ [L]
  RewriteRule (.*) public/$1 [L]
</IfModule>


With that done one gets a functioning progress bar. I have absolutely no idea if this is the correct approach and what effect altering the .htaccess file like that would have outside the localhost environment. I'm not sure if this points the way to adapting what I want to use to follow this pattern or if, as I suspect is the case, I missed something earlier.
Level One Member

Dom

User Level: Level One Member

Date Joined: 12/01/2024

Posted by djnordeen on Thursday 18th April 2024 at 20:11 GMT

Hey Dom,
Open a app, search for spinner in files,
Trongate_filezone has a spinner.
Dan
Early Adopter

djnordeen

User Level: Early Adopter

Date Joined: 20/08/2021

Posted by DaFa on Sunday 21st April 2024 at 12:47 GMT

Hi Dan, I think Dom was more after a progress bar than the spinner

Hey Dom,
just had some fun creating this
<!-- HTML element for a progress bar -->
<div class="progress-bar">
    <div class="progress" id="progress"></div>
</div>
/* CSS for progresss bar */
:root {
  --max-progress-width: 500px;
  --progress-height: 30px;
  --border-radius: 10px;
}

.progress-bar {
  max-width: var(--max-progress-width);
  height: var(--progress-height);
  border-radius: var(--border-radius);
  overflow: hidden;
  position: relative;
}

.progress {
  background-color: #007bff;
  transition: width 0.5s ease-in-out;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 16px;
  position: absolute;
  width: 0;
}
// JavaScript for progresss bar
const progressBar = document.getElementById("progress");

function updateProgress(percent) {
  progressBar.style.width = `${percent}%`;
  if (percent > 0) {
    progressBar.textContent = `${percent}%`;
  } else {
    progressBar.textContent = "";
  }
}
then you can call
updateProgress(25); // Updates the progress bar to 25%
updateProgress(50); // Updates the progress bar to 50%
updateProgress(100); // Updates the progress bar to 100%
to update the progress bar...

This comment was edited by DaFa on Sunday 21st April 2024 at 13:02 GMT

Founding Member

DaFa

User Level: Founding Member

Date Joined: 30/11/2018

Posted by Dom on Monday 22nd April 2024 at 10:50 GMT

Hi Simon

Thank you for the sample, these are fun aren't they!!

When all self contained on the view this works beautifully but I've not had as much luck when trying to trigger the updateProgress from the controller. Logically I'm sure that this ought to work.
// Update progress
                    $progress = intval(($index + 1) / $totalImages * 100);

                    // Output progress update as JavaScript
                    echo "<script>updateProgress($progress);</script>";
                    flush(); // Flush the output buffer to ensure immediate sending


However as yet it's not quite there. I really need to devote some more time to getting xdebg and phpstorm working. I so miss the ease with which it is possible to debug .net stuff.
Level One Member

Dom

User Level: Level One Member

Date Joined: 12/01/2024

Posted by DaFa on Monday 22nd April 2024 at 14:12 GMT

The trick to do that is via AJAX and a bit of interval shenanigans - I have been playing around with it for a bit tonight and have the progress bar displaying to 90% then it stops even though I complete all inserts in the database. Also, the echos are not displaying in my view as expected - I can't get the PHP buffer to display in the view either with flush();
It's getting late here so will try to fix it tomorrow... if I get time.

But here is the code in case you want to have a stab at solving it:
<!-- View - HTML progress bar -->
<div class="progress-bar">
    <div class="progress" id="progress"></div>
</div>
<button id="startButton">Start Progress</button>
<input type="number" id="rowCount" placeholder="Enter rows to generate" min="1" value="10000">
<p id="output"></p>
// JavaScript for progress bar
const progressBar = document.getElementById("progress");
const startButton = document.getElementById("startButton");
const rowCount = document.getElementById("rowCount");
const output = document.getElementById("output");
let interval;
let finishedTimeout;
let isProgressComplete = false;
let currentRowIndex = 0;
const batchSize = 1000; // Number of rows to insert per AJAX request

function updateProgress(percent, message) {
  progressBar.style.width = `${percent}%`;
  if (percent >= 0 && percent < 100) {
    progressBar.textContent = `${percent}%`;
  } else if (percent >= 100) {
    progressBar.textContent = "100%";
    clearInterval(interval);
    isProgressComplete = true; // Set the flag to true
  } else {
    progressBar.textContent = "";
  }

  if (isProgressComplete) {
    startButton.textContent = "Finished";
    finishedTimeout = setTimeout(resetProgress, 3000); // Reset progress after 3 seconds
  }

  // Only update the output element if the message doesn't contain "Progress: XX%"
  if (!message.includes("Progress:")) {
    output.textContent = message;
  }
}

function resetProgress() {
  progressBar.style.width = "0%";
  progressBar.textContent = "";
  startButton.textContent = "Start Progress";
  startButton.disabled = false;
  clearTimeout(finishedTimeout); // Clear the timeout to avoid conflicts
  isProgressComplete = false; // Reset the flag
  output.textContent = "";
  currentRowIndex = 0; // Reset the current row index
}

startButton.addEventListener("click", function () {
  startButton.disabled = true;
  isProgressComplete = false; // Reset the flag
  const totalRows = parseInt(rowCount.value, 10) || 10000;

  // Send an AJAX request every 100ms to get the progress update
  interval = setInterval(function () {
    const xhr = new XMLHttpRequest();
    xhr.open("POST", "http://localhost/help_bar/e/trn_demo", true);
    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    xhr.onreadystatechange = function () {
      if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
        const response = xhr.responseText.trim();
        if (response.includes("Inserting")) {
          updateProgress(0, response);
        } else if (response.includes("Final inserted row ID")) {
          updateProgress(100, response);
        } else {
          const progressMatch = response.match(/Progress: (\d+)%/);
          if (progressMatch) {
            const progress = parseInt(progressMatch[1], 10);
            updateProgress(progress, ""); // Pass an empty string to avoid updating the output
          } else {
            updateProgress(
              (xhr.responseText.match(/(\d+)%/) || [0, 0])[1],
              response
            );
          }
        }
      }
    };
    xhr.send(`rows=${totalRows}&start=${currentRowIndex}&batch=${batchSize}`);
    currentRowIndex += batchSize;
    if (currentRowIndex >= totalRows) {
      clearInterval(interval);
    }
  }, 100);
});
This is the demo endpoint method in my test controller (http://localhost/help_bar/e/trn_demo) - notice the transactions 🤓
function trn_demo() {
    $totalRows = $_POST['rows'] ?? 1; // default to 1 row if none specified
    $startIndex = $_POST['start'] ?? 0;
    $batchSize = $_POST['batch'] ?? 1000; // limit batches to 1000 rows in not specified
    $endIndex = min($startIndex + $batchSize, $totalRows);

    if ($startIndex === 0) {
        echo "Inserting " . number_format($totalRows) . " rows...
";
        echo "Starting transaction...
";
        $this->model->start_transaction();
    }

    try {
        // Loop to insert a batch of fake data
        for ($i = $startIndex + 1; $i <= $endIndex; $i++) {
            $data['name'] = "Test Name $i";
            $data['notes'] = "Test Notes $i";
            $data['picture'] = "test_image_$i.jpg";
            $update_id = $this->model->insert($data, 'images');
            $progress = round(($i / $totalRows) * 100);
            echo "Progress: $progress%
";
            flush(); // Flush the output buffer to update the client immediately
        }

        if ($endIndex === $totalRows) {
            // If all operations are successful, commit the transaction
            echo "Committing transaction...
";
            $this->model->commit_transaction();
            echo "Final inserted row ID: " . number_format($update_id);
        }
    } catch (Exception $e) {
        // If any operation fails, rollback the transaction
        $this->model->rollback_transaction();
        echo 'Error: ' . $e->getMessage();
    }

    if ($endIndex === $totalRows) {
        // End the transaction
        $this->model->end_transaction();
    }
}
While the above all works by inserting rows into my images table
CREATE TABLE `images` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `notes` text DEFAULT NULL,
  `picture` varchar(255) DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
Here also is the transaction code I added to Model.php. I'll create a pull request once I test it some more... might even add type hinting and return types
/**
 * Starts a database transaction.
 *
 * This function begins a transaction on the database connection associated with
 * the current instance of the class. Transactions allow multiple database
 * operations to be executed as a single unit of work, ensuring data consistency
 * and integrity.
 *
 * @throws PDOException if the transaction cannot be started
 * @return void
 */
public function start_transaction() {
    $this->dbh->beginTransaction();
}

/**
 * Commits the current database transaction.
 *
 * This function commits the current database transaction, making all changes
 * made during the transaction permanent. It should be called after a successful
 * series of database operations within a transaction.
 *
 * @throws PDOException if there is an error committing the transaction
 * @return void
 */
public function commit_transaction() {
    $this->dbh->commit();
}

/**
 * Rolls back the current database transaction.
 *
 * @throws PDOException if there is an error rolling back the transaction
 * @return void
 */
public function rollback_transaction() {
    $this->dbh->rollBack();
}

/**
 * Ends the current database transaction if one is in progress.
 *
 * This function checks if a transaction is currently in progress using the `inTransaction()` method of the `$dbh` object.
 * If a transaction is in progress, it is rolled back using the `rollBack()` method of the `$dbh` object.
 *
 * @return void
 */
public function end_transaction() {
    if ($this->dbh->inTransaction()) {
        $this->dbh->rollBack();
    }
}
Cheers and happy coding!

This comment was edited by DaFa on Monday 22nd April 2024 at 14:23 GMT

Founding Member

DaFa

User Level: Founding Member

Date Joined: 30/11/2018

Posted by Dom on Monday 22nd April 2024 at 15:37 GMT

Hi Simon
Flushing has been the major issue at this end. Inserts into the data base have generally been so quick that I haven't bothered with those yet (however once transactions come into play and one could conceivably add millions of records) then it would be relevant.

I've been using it for the image folder creation and transfer process which is still quick but nowhere near as quick as inserts.
Level One Member

Dom

User Level: Level One Member

Date Joined: 12/01/2024

Posted by Dom on Tuesday 23rd April 2024 at 08:42 GMT

I spent quite a bit of time messing around last night and again early this morning.

If things are initiated from the view itself, much easier with the insert statement because you have easy access to the number of rows of fake data to be created, and then progress is monitored through a js loop in the views script you get progress working reasonably well. As you observed it's still not perfect.

I've been trying to get this to monitor the copying of images and associated creation of properly structured directories at the point that you want to create data for a module that has a single image uploader associated with it. This process is about 3 to 4 times as expensive as simply inserting data and so a form of progress monitoring that gives an indication of progress seems appropriate.

The problem is that it needs to be initiated on the backend and whilst it all seems to work the progress notifications all seem to be cached on the backend until final completion and then sent as a single job lot.

My php and js debugging skills still leave a lot to be desired so I'm not able to fully trace the process as I would do in .net. So that i suspect is the next task, bring myself up to speed on that front and then see where we get to.

The sample that I posted earlier (3rd post in this stream) does work, nicely as it happens, but I had a nightmare trying to transfer it to the actual situation at hand. Exactly the same issue exists in .net where you have to work across threads and it can be an absolute nightmare to get it to work.
Level One Member

Dom

User Level: Level One Member

Date Joined: 12/01/2024

×