GH-2107: Add a progress bar for the overall status of the file uploads.
diff --git a/jena-fuseki2/jena-fuseki-ui/src/services/mock/json-server.js b/jena-fuseki2/jena-fuseki-ui/src/services/mock/json-server.js
index b49f5dd..5f20e1d 100644
--- a/jena-fuseki2/jena-fuseki-ui/src/services/mock/json-server.js
+++ b/jena-fuseki2/jena-fuseki-ui/src/services/mock/json-server.js
@@ -260,6 +260,15 @@
.send(dataContent)
})
+let failUpload = false
+
+// Upload data.
+server.post('/:datasetName/data', (req, res) => {
+ res
+ .status(200)
+ .send()
+})
+
// PING
// GET PING STATUS
server.get('/\\$/ping', (req, res) => {
diff --git a/jena-fuseki2/jena-fuseki-ui/src/views/dataset/Upload.vue b/jena-fuseki2/jena-fuseki-ui/src/views/dataset/Upload.vue
index dc9c87a..3f75631 100644
--- a/jena-fuseki2/jena-fuseki-ui/src/views/dataset/Upload.vue
+++ b/jena-fuseki2/jena-fuseki-ui/src/views/dataset/Upload.vue
@@ -35,7 +35,7 @@
</div>
<div class="row">
<div class="col-sm-12">
- <h3>Upload files</h3>
+ <h3>Upload files {{ postActionUrl }}</h3>
<p>
Load data into the default graph of the currently selected dataset, or the given named graph.
You may upload any RDF format, such as Turtle, RDF/XML or TRiG.
@@ -100,7 +100,7 @@
v-if="!$refs.upload || !$refs.upload.active"
@click.prevent="uploadAll()"
type="button"
- class="btn btn-primary ms-2 d-inline"
+ class="btn btn-primary ms-2 d-inline upload-files"
>
<FontAwesomeIcon icon="upload" />
<span class="ms-2">upload all</span>
@@ -119,6 +119,30 @@
</div>
</div>
</div>
+ <div class="pt-2 pb-2">
+ <div class="progress" style="height: 1.5rem;">
+ <div
+ class="progress-bar"
+ role="progressbar"
+ :style="`width: ${uploadSucceededPercentage}%`"
+ :aria-valuenow="uploadSucceededPercentage"
+ aria-valuemin="0"
+ aria-valuemax="100"
+ >
+ {{ uploadSucceededCount }}/{{ uploadCount }}
+ </div>
+ <div
+ class="progress-bar bg-danger"
+ role="progressbar"
+ :style="`width: ${uploadFailedPercentage}%`"
+ :aria-valuenow="uploadFailedPercentage"
+ aria-valuemin="0"
+ aria-valuemax="100"
+ >
+ {{ uploadFailedCount }}/{{ uploadCount }}
+ </div>
+ </div>
+ </div>
</form>
</div>
</div>
@@ -161,7 +185,7 @@
<button
@click.prevent="data.item.success || data.item.error === 'compressing' ? false : $refs.upload.update(data.item, {active: true})"
type="button"
- class="btn btn-outline-primary me-0 mb-2 d-block"
+ class="btn btn-outline-primary me-0 mb-2 d-block upload-file"
>
<FontAwesomeIcon icon="upload" />
<span class="ms-2">upload now</span>
@@ -169,7 +193,7 @@
<button
@click.prevent="remove(data.item)"
type="button"
- class="btn btn-outline-primary me-0 mb-md-0 d-block d-md-inline-block"
+ class="btn btn-outline-primary me-0 mb-md-0 d-block d-md-inline-block remove-file"
>
<FontAwesomeIcon icon="minus-circle" />
<span class="ms-2">remove</span>
@@ -254,7 +278,7 @@
// name: ''
// }
},
- datasetTableFields: [
+ datasetTableFields: Object.freeze([
{
key: 'name',
label: 'name',
@@ -279,7 +303,7 @@
key: 'actions',
label: 'actions'
}
- ]
+ ])
}
},
@@ -302,6 +326,36 @@
const params = (this.datasetGraphName && this.datasetGraphName !== '') ? `?graph=${this.datasetGraphName}` : ''
const dataEndpoint = this.services['gsp-rw']['srv.endpoints'].find(endpoint => endpoint !== '') || ''
return this.$fusekiService.getFusekiUrl(`/${this.datasetName}/${dataEndpoint}${params}`)
+ },
+ uploadCount () {
+ if (!this.upload || !this.upload.files) {
+ return 0
+ }
+ return this.upload.files.length
+ },
+ uploadSucceededCount () {
+ if (!this.upload || !this.upload.files) {
+ return 0
+ }
+ return this.upload.files.filter(f => Boolean(f.success)).length
+ },
+ uploadFailedCount () {
+ if (!this.upload || !this.upload.files) {
+ return 0
+ }
+ return this.upload.files.filter(f => Boolean(f.error)).length
+ },
+ uploadFailedPercentage () {
+ if (this.uploadCount === 0) {
+ return 0
+ }
+ return (this.uploadFailedCount / this.uploadCount) * 100
+ },
+ uploadSucceededPercentage () {
+ if (this.uploadCount === 0) {
+ return 0
+ }
+ return (this.uploadSucceededCount / this.uploadCount) * 100
}
},
diff --git a/jena-fuseki2/jena-fuseki-ui/src/views/manage/ExistingDatasets.vue b/jena-fuseki2/jena-fuseki-ui/src/views/manage/ExistingDatasets.vue
index 59c3991..0ea6b32 100644
--- a/jena-fuseki2/jena-fuseki-ui/src/views/manage/ExistingDatasets.vue
+++ b/jena-fuseki2/jena-fuseki-ui/src/views/manage/ExistingDatasets.vue
@@ -44,7 +44,7 @@
<button
@click="$router.push(`/dataset${data.item.name}/query`)"
type="button"
- class="btn btn-primary me-0 me-md-2 mb-2 mb-md-0 d-block d-md-inline-block"
+ class="btn btn-primary me-0 me-md-2 mb-2 mb-md-0 d-block d-md-inline-block dataset-query"
>
<FontAwesomeIcon icon="question-circle" />
<span class="ms-1">query</span>
@@ -81,7 +81,7 @@
@click="showPopover(`delete-dataset-${data.item.name}`)"
type="button"
href="#"
- class="btn btn-primary me-0 me-md-2 mb-2 mb-md-0 d-block d-md-inline-block"
+ class="btn btn-primary me-0 me-md-2 mb-2 mb-md-0 d-block d-md-inline-block dataset-remove"
>
<FontAwesomeIcon icon="times-circle" />
<span class="ms-1">remove</span>
@@ -118,7 +118,7 @@
@click="showPopover(`backup-dataset-${data.item.name}`)"
type="button"
href="#"
- class="btn btn-primary me-0 me-md-2 me-0 mb-2 mb-md-0 d-block d-md-inline-block"
+ class="btn btn-primary me-0 me-md-2 me-0 mb-2 mb-md-0 d-block d-md-inline-block dataset-backup"
>
<FontAwesomeIcon icon="download" />
<span class="ms-1">backup</span>
@@ -126,7 +126,7 @@
<button
@click="$router.push(`/dataset${data.item.name}/upload`)"
type="button"
- class="btn btn-primary me-0 me-md-2 me-0 mb-2 mb-md-0 d-block d-md-inline-block"
+ class="btn btn-primary me-0 me-md-2 me-0 mb-2 mb-md-0 d-block d-md-inline-block dataset-add-data"
>
<FontAwesomeIcon icon="upload" />
<span class="ms-1">add data</span>
@@ -134,7 +134,7 @@
<button
@click="$router.push(`/dataset${data.item.name}/info`)"
type="button"
- class="btn btn-primary me-0 mb-md-0 d-block d-md-inline-block"
+ class="btn btn-primary me-0 mb-md-0 d-block d-md-inline-block dataset-info"
>
<FontAwesomeIcon icon="tachometer-alt" />
<span class="ms-1">info</span>
diff --git a/jena-fuseki2/jena-fuseki-ui/tests/e2e/specs/upload.cy.js b/jena-fuseki2/jena-fuseki-ui/tests/e2e/specs/upload.cy.js
new file mode 100644
index 0000000..c31d1cc
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-ui/tests/e2e/specs/upload.cy.js
@@ -0,0 +1,155 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Tests the upload view.
+ *
+ * Instead of injecting test data, we use the Manage New view to
+ * add new datasets. So we also cover parts of the Manage view.
+ */
+
+describe('upload', () => {
+ beforeEach(() => {
+ // Intercept new dataset request.
+ cy.intercept('POST', '/$/datasets').as('createDataset')
+ // Special endpoint that clears the datasets data.
+ cy.request('/tests/reset')
+ // Create a sample dataset.
+ cy
+ .visit('/#/manage/new')
+ .then(() => {
+ cy
+ .get('#dataset-name')
+ .type('skosmos')
+ cy
+ .get('#data-set-type-mem')
+ .click()
+ cy
+ .get('button[type="submit"]')
+ .click()
+ // We are redirected to the Manage datasets view.
+ cy
+ .get('table.jena-table')
+ .should('be.visible')
+ })
+ // Wait for the create dataset request to be processed and have a return status, then continue.
+ cy.wait('@createDataset')
+
+ cy.intercept('/$/server').as('server')
+ // We wait until the route navigation guard is called, so that the
+ // view has the dataset information loaded.
+ cy.visit('/#/dataset/skosmos/upload')
+ cy.wait('@server')
+ cy.intercept('/$/server').as('server')
+ })
+ afterEach(() => {
+ // Special endpoint that clears the datasets data.
+ cy.request('/tests/reset')
+ })
+ it('displays an empty progress bar by default', () => {
+ // The progress is present.
+ cy
+ .get('.progress')
+ .should('be.visible')
+ // We have two inner progress bars.
+ cy
+ .get('.progress-bar')
+ .should('have.length', 2)
+ // And each progress bar has the current value set to zero.
+ cy
+ .get('.progress-bar')
+ .each(($el) => {
+ cy
+ .wrap($el)
+ .should('have.attr', 'aria-valuenow', 0)
+ })
+ })
+ it('displays the progress for success and failure', () => {
+ // Intercept upload calls.
+ // Fails every other upload.
+ let fail = false
+ cy.intercept('POST', '/skosmos/data', (req) => {
+ console.log('in upload request')
+ console.log(req)
+ const statusCode = fail ? 500 : 200
+ fail = !fail
+ req.reply({
+ statusCode
+ })
+ }).as('upload')
+ cy
+ .get('input[type=file]')
+ .should('exist')
+ // Prepare three files to be uploaded (Second will fail! See intercept above!).
+ const NUMBER_OF_FILES = 3
+ for (let idx = 0; idx < NUMBER_OF_FILES; idx++) {
+ cy
+ .get('input[type=file]')
+ .selectFile({
+ contents: Cypress.Buffer.from(`@prefix ex: <http://test.com'onclick=alert(123);'> .
+ ex:ABC a ex:DEF .`),
+ fileName: `file${idx}.ttl`,
+ lastModified: Date.now(),
+ },
+ {
+ force: true
+ })
+ }
+ // We have three files, the json-server handler is programmed to
+ // fail every other time, so we will have 2 successes and one failure.
+ cy
+ .get('button.upload-file')
+ .each(($el) => {
+ cy
+ .wrap($el)
+ .click({force: true})
+ cy.wait('@upload')
+ })
+ // Overall progress now shows 2/3 success, 1/3 failure.
+ cy
+ .get('.progress')
+ .eq(0)
+ .find('.progress-bar')
+ .eq(0)
+ .should('have.text', '2/3')
+ cy
+ .get('.progress')
+ .eq(0)
+ .find('.progress-bar')
+ .eq(1)
+ .should('have.text', '1/3')
+ // First and third files are shown as success, second is failure.
+ cy
+ .get('.progress')
+ .eq(1)
+ .find('.progress-bar')
+ .eq(0)
+ .should('have.class', 'bg-success')
+ cy
+ .get('.progress')
+ .eq(2)
+ .find('.progress-bar')
+ .eq(0)
+ .should('have.class', 'bg-danger')
+ cy
+ .get('.progress')
+ .eq(3)
+ .find('.progress-bar')
+ .eq(0)
+ .should('have.class', 'bg-success')
+ })
+})