Page MenuHomeSoftware Heritage

No OneTemporary

diff --git a/assets/src/bundles/add_forge/create-request.js b/assets/src/bundles/add_forge/create-request.js
index 07c3d2f9..3d23afb0 100644
--- a/assets/src/bundles/add_forge/create-request.js
+++ b/assets/src/bundles/add_forge/create-request.js
@@ -1,128 +1,155 @@
/**
* Copyright (C) 2022 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
import {handleFetchError, removeUrlFragment, csrfPost,
getHumanReadableDate} from 'utils/functions';
+import userRequestsFilterCheckboxFn from 'utils/requests-filter-checkbox.ejs';
+import {swhSpinnerSrc} from 'utils/constants';
let requestBrowseTable;
+const addForgeCheckboxId = 'swh-add-forge-user-filter';
+const userRequestsFilterCheckbox = userRequestsFilterCheckboxFn({
+ 'inputId': addForgeCheckboxId,
+ 'checked': true // by default, display only user requests
+});
+
export function onCreateRequestPageLoad() {
$(document).ready(() => {
$('#requestCreateForm').submit(async function(event) {
event.preventDefault();
try {
const response = await csrfPost($(this).attr('action'),
{'Content-Type': 'application/x-www-form-urlencoded'},
$(this).serialize());
handleFetchError(response);
$('#userMessageDetail').empty();
$('#userMessage').text('Your request has been submitted');
$('#userMessage').removeClass('badge-danger');
$('#userMessage').addClass('badge-success');
requestBrowseTable.draw(); // redraw the table to update the list
} catch (errorResponse) {
$('#userMessageDetail').empty();
let errorMessage;
let errorMessageDetail = '';
const errorData = await errorResponse.json();
// if (errorResponse.content_type === 'text/plain') { // does not work?
if (errorResponse.status === 409) {
errorMessage = errorData;
} else { // assuming json response
// const exception = errorData['exception'];
errorMessage = 'An unknown error occurred during the request creation';
try {
const reason = JSON.parse(errorData['reason']);
Object.entries(reason).forEach((keys, _) => {
const key = keys[0];
const message = keys[1][0]; // take only the first issue
errorMessageDetail += `\n${key}: ${message}`;
});
} catch (_) {
errorMessageDetail = errorData['reason']; // can't parse it, leave it raw
}
}
$('#userMessage').text(
errorMessageDetail ? `Error: ${errorMessageDetail}` : errorMessage
);
$('#userMessage').removeClass('badge-success');
$('#userMessage').addClass('badge-danger');
}
});
$('#swh-add-forge-requests-list-tab').on('shown.bs.tab', () => {
+ requestBrowseTable.draw();
window.location.hash = '#browse-requests';
});
$('#swh-add-forge-requests-help-tab').on('shown.bs.tab', () => {
window.location.hash = '#help';
});
$('#swh-add-forge-tab').on('shown.bs.tab', () => {
removeUrlFragment();
});
$(window).on('hashchange', () => {
onPageHashChage();
});
onPageHashChage(); // Explicit call to handle a hash during the page load
populateRequestBrowseList(); // Load existing requests
});
}
function onPageHashChage() {
if (window.location.hash === '#browse-requests') {
$('.nav-tabs a[href="#swh-add-forge-requests-list"]').tab('show');
} else if (window.location.hash === '#help') {
$('.nav-tabs a[href="#swh-add-forge-requests-help"]').tab('show');
} else {
$('.nav-tabs a[href="#swh-add-forge-submit-request"]').tab('show');
}
}
export function populateRequestBrowseList() {
requestBrowseTable = $('#add-forge-request-browse')
.on('error.dt', (e, settings, techNote, message) => {
$('#add-forge-browse-request-error').text(message);
})
.DataTable({
serverSide: true,
processing: true,
+ language: {
+ processing: `<img src="${swhSpinnerSrc}"></img>`
+ },
retrieve: true,
searching: true,
info: false,
- dom: '<<"d-flex justify-content-between align-items-center"f' +
- '<"#list-exclude">l>rt<"bottom"ip>>',
+ // Layout configuration, see [1] for more details
+ // [1] https://datatables.net/reference/option/dom
+ dom: '<"row"<"col-sm-3"l><"col-sm-6 text-left user-requests-filter"><"col-sm-3"f>>' +
+ '<"row"<"col-sm-12"tr>>' +
+ '<"row"<"col-sm-5"i><"col-sm-7"p>>',
ajax: {
- 'url': Urls.add_forge_request_list_datatables()
+ 'url': Urls.add_forge_request_list_datatables(),
+ data: (d) => {
+ if (swh.webapp.isUserLoggedIn() && $(`#${addForgeCheckboxId}`).prop('checked')) {
+ d.user_requests_only = '1';
+ }
+ }
+ },
+ fnInitComplete: function() {
+ if (swh.webapp.isUserLoggedIn()) {
+ $('div.user-requests-filter').html(userRequestsFilterCheckbox);
+ $(`#${addForgeCheckboxId}`).on('change', () => {
+ requestBrowseTable.draw();
+ });
+ }
},
columns: [
{
data: 'submission_date',
name: 'submission_date',
render: getHumanReadableDate
},
{
data: 'forge_type',
name: 'forge_type'
},
{
data: 'forge_url',
name: 'forge_url'
},
{
data: 'status',
name: 'status',
render: function(data, type, row, meta) {
return swh.add_forge.formatRequestStatusName(data);
}
}
]
});
- requestBrowseTable.draw();
}
diff --git a/cypress/integration/add-forge-now-request-create.spec.js b/cypress/integration/add-forge-now-request-create.spec.js
index 4b533060..d9612f4e 100644
--- a/cypress/integration/add-forge-now-request-create.spec.js
+++ b/cypress/integration/add-forge-now-request-create.spec.js
@@ -1,183 +1,255 @@
/**
* Copyright (C) 2022 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
function populateForm(type, url, contact, email, consent, comment) {
cy.get('#swh-input-forge-type').select(type);
- cy.get('#swh-input-forge-url').type(url);
- cy.get('#swh-input-forge-contact-name').type(contact);
- cy.get('#swh-input-forge-contact-email').type(email);
- cy.get('#swh-input-forge-comment').type(comment);
+ cy.get('#swh-input-forge-url').clear().type(url, {delay: 0, force: true});
+ cy.get('#swh-input-forge-contact-name').clear().type(contact, {delay: 0, force: true});
+ cy.get('#swh-input-forge-contact-email').clear().type(email, {delay: 0, force: true});
+ if (comment) {
+ cy.get('#swh-input-forge-comment').clear().type(comment, {delay: 0, force: true});
+ }
cy.get('#swh-input-consent-check').click({force: consent === 'on'});
}
+describe('Browse requests list tests', function() {
+ beforeEach(function() {
+ this.addForgeNowUrl = this.Urls.forge_add();
+ this.listAddForgeRequestsUrl = this.Urls.add_forge_request_list_datatables();
+ });
+
+ it('should not show user requests filter checkbox for anonymous users', function() {
+ cy.visit(this.addForgeNowUrl);
+ cy.get('#swh-add-forge-requests-list-tab').click();
+ cy.get('#swh-add-forge-user-filter').should('not.exist');
+ });
+
+ it('should show user requests filter checkbox for authenticated users', function() {
+ cy.userLogin();
+ cy.visit(this.addForgeNowUrl);
+ cy.get('#swh-add-forge-requests-list-tab').click();
+ cy.get('#swh-add-forge-user-filter').should('exist').should('be.checked');
+ });
+
+ it('should only display user requests when filter is activated', function() {
+ // Clean up previous state
+ cy.task('db:add_forge_now:delete');
+ // 'user2' logs in and create requests
+ cy.user2Login();
+ cy.visit(this.addForgeNowUrl);
+
+ // create requests for the user 'user'
+ populateForm('gitlab', 'gitlab.org', 'admin', 'admin@example.org', 'on', '');
+ cy.get('#requestCreateForm').submit();
+
+ // user requests filter checkbox should be in the DOM
+ cy.get('#swh-add-forge-requests-list-tab').click();
+ cy.get('#swh-add-forge-user-filter').should('exist').should('be.checked');
+
+ // check unfiltered user requests
+ cy.get('tbody tr').then(rows => {
+ expect(rows.length).to.eq(1);
+ });
+
+ // user1 logout
+ cy.contains('a', 'logout').click();
+
+ // user logs in
+ cy.userLogin();
+ cy.visit(this.addForgeNowUrl);
+
+ populateForm('gitea', 'gitea.org', 'admin', 'admin@example.org', 'on', '');
+ cy.get('#requestCreateForm').submit();
+ populateForm('cgit', 'cgit.org', 'admin', 'admin@example.org', 'on', '');
+ cy.get('#requestCreateForm').submit();
+
+ // user requests filter checkbox should be in the DOM
+ cy.get('#swh-add-forge-requests-list-tab').click();
+ cy.get('#swh-add-forge-user-filter').should('exist').should('be.checked');
+
+ // check unfiltered user requests
+ cy.get('tbody tr').then(rows => {
+ expect(rows.length).to.eq(2);
+ });
+
+ cy.get('#swh-add-forge-user-filter')
+ .uncheck({force: true});
+
+ // Users now sees everything
+ cy.get('tbody tr').then(rows => {
+ expect(rows.length).to.eq(2 + 1);
+ });
+ });
+});
+
describe('Test add-forge-request creation', function() {
beforeEach(function() {
this.addForgeNowUrl = this.Urls.forge_add();
});
it('should show all the tabs for every user', function() {
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-tab')
.should('have.class', 'nav-link');
cy.get('#swh-add-forge-requests-list-tab')
.should('have.class', 'nav-link');
cy.get('#swh-add-forge-requests-help-tab')
.should('have.class', 'nav-link');
});
it('should show create forge tab by default', function() {
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-tab')
.should('have.class', 'active');
cy.get('#swh-add-forge-requests-list-tab')
.should('not.have.class', 'active');
});
it('should show login link for anonymous user', function() {
cy.visit(this.addForgeNowUrl);
cy.get('#loginLink')
.should('be.visible')
.should('contain', 'log in');
});
it('should bring back after login', function() {
cy.visit(this.addForgeNowUrl);
cy.get('#loginLink')
.should('have.attr', 'href')
.and('include', `${this.Urls.login()}?next=${this.Urls.forge_add()}`);
});
it('should change tabs on click', function() {
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-requests-list-tab').click();
cy.get('#swh-add-forge-tab')
.should('not.have.class', 'active');
cy.get('#swh-add-forge-requests-list-tab')
.should('have.class', 'active');
cy.get('#swh-add-forge-requests-help-tab')
.should('not.have.class', 'active');
cy.hash().should('eq', '#browse-requests');
cy.get('#swh-add-forge-requests-help-tab').click();
cy.get('#swh-add-forge-tab')
.should('not.have.class', 'active');
cy.get('#swh-add-forge-requests-list-tab')
.should('not.have.class', 'active');
cy.get('#swh-add-forge-requests-help-tab')
.should('have.class', 'active');
cy.hash().should('eq', '#help');
cy.get('#swh-add-forge-tab').click();
cy.get('#swh-add-forge-tab')
.should('have.class', 'active');
cy.get('#swh-add-forge-requests-list-tab')
.should('not.have.class', 'active');
cy.get('#swh-add-forge-requests-help-tab')
.should('not.have.class', 'active');
cy.hash().should('eq', '');
});
it('should show create form elements to authenticated user', function() {
cy.userLogin();
cy.visit(this.addForgeNowUrl);
cy.get('#swh-input-forge-type')
.should('be.visible');
cy.get('#swh-input-forge-url')
.should('be.visible');
cy.get('#swh-input-forge-contact-name')
.should('be.visible');
cy.get('#swh-input-consent-check')
.should('be.visible');
cy.get('#swh-input-forge-comment')
.should('be.visible');
cy.get('#swh-input-form-submit')
.should('be.visible');
});
it('should show browse requests table for every user', function() {
// testing only for anonymous
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-requests-list-tab').click();
cy.get('#add-forge-request-browse')
.should('be.visible');
cy.get('#loginLink')
.should('not.be.visible');
});
it('should update browse list on successful submission', function() {
cy.userLogin();
cy.visit(this.addForgeNowUrl);
populateForm('bitbucket', 'gitlab.com', 'test', 'test@example.com', 'on', 'test comment');
cy.get('#requestCreateForm').submit();
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-requests-list-tab').click();
cy.get('#add-forge-request-browse')
.should('be.visible')
.should('contain', 'gitlab.com');
cy.get('#add-forge-request-browse')
.should('be.visible')
.should('contain', 'Pending');
});
it('should show error message on conflict', function() {
cy.userLogin();
cy.visit(this.addForgeNowUrl);
populateForm('bitbucket', 'gitlab.com', 'test', 'test@example.com', 'on', 'test comment');
cy.get('#requestCreateForm').submit();
cy.get('#requestCreateForm').submit(); // Submitting the same data again
cy.get('#userMessage')
.should('have.class', 'badge-danger')
.should('contain', 'already exists');
});
it('should show error message', function() {
cy.userLogin();
cy.intercept('POST', `${this.Urls.api_1_add_forge_request_create()}**`,
{
body: {
'exception': 'BadInputExc',
'reason': '{"add-forge-comment": ["This field is required"]}'
},
statusCode: 400
}).as('errorRequest');
cy.visit(this.addForgeNowUrl);
populateForm(
'bitbucket', 'gitlab.com', 'test', 'test@example.com', 'off', 'comment'
);
cy.get('#requestCreateForm').submit();
cy.wait('@errorRequest').then((xhr) => {
cy.get('#userMessage')
.should('have.class', 'badge-danger')
.should('contain', 'field is required');
});
});
});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
index 01245fcc..eeb7373b 100644
--- a/cypress/plugins/index.js
+++ b/cypress/plugins/index.js
@@ -1,152 +1,160 @@
/**
* Copyright (C) 2019-2022 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
const axios = require('axios');
const fs = require('fs');
const sqlite3 = require('sqlite3').verbose();
async function httpGet(url) {
const response = await axios.get(url);
return response.data;
}
async function getMetadataForOrigin(originUrl, baseUrl) {
const originVisitsApiUrl = `${baseUrl}/api/1/origin/${originUrl}/visits`;
const originVisits = await httpGet(originVisitsApiUrl);
const lastVisit = originVisits[0];
const snapshotApiUrl = `${baseUrl}/api/1/snapshot/${lastVisit.snapshot}`;
const lastOriginSnapshot = await httpGet(snapshotApiUrl);
let revision = lastOriginSnapshot.branches.HEAD.target;
if (lastOriginSnapshot.branches.HEAD.target_type === 'alias') {
revision = lastOriginSnapshot.branches[revision].target;
}
const revisionApiUrl = `${baseUrl}/api/1/revision/${revision}`;
const lastOriginHeadRevision = await httpGet(revisionApiUrl);
return {
'directory': lastOriginHeadRevision.directory,
'revision': lastOriginHeadRevision.id,
'snapshot': lastOriginSnapshot.id
};
};
function getDatabase() {
return new sqlite3.Database('./swh-web-test.sqlite3');
}
module.exports = (on, config) => {
require('@cypress/code-coverage/task')(on, config);
// produce JSON files prior launching browser in order to dynamically generate tests
on('before:browser:launch', function(browser, launchOptions) {
return new Promise((resolve) => {
const p1 = axios.get(`${config.baseUrl}/tests/data/content/code/extensions/`);
const p2 = axios.get(`${config.baseUrl}/tests/data/content/code/filenames/`);
Promise.all([p1, p2])
.then(function(responses) {
fs.writeFileSync('cypress/fixtures/source-file-extensions.json', JSON.stringify(responses[0].data));
fs.writeFileSync('cypress/fixtures/source-file-names.json', JSON.stringify(responses[1].data));
resolve();
});
});
});
on('task', {
getSwhTestsData: async() => {
if (!global.swhTestsData) {
const swhTestsData = {};
swhTestsData.unarchivedRepo = {
url: 'https://github.com/SoftwareHeritage/swh-web',
type: 'git',
revision: '7bf1b2f489f16253527807baead7957ca9e8adde',
snapshot: 'd9829223095de4bb529790de8ba4e4813e38672d',
rootDirectory: '7d887d96c0047a77e2e8c4ee9bb1528463677663',
content: [{
sha1git: 'b203ec39300e5b7e97b6e20986183cbd0b797859'
}]
};
swhTestsData.origin = [{
url: 'https://github.com/memononen/libtess2',
type: 'git',
content: [{
path: 'Source/tess.h'
}, {
path: 'premake4.lua'
}],
directory: [{
path: 'Source',
id: 'cd19126d815470b28919d64b2a8e6a3e37f900dd'
}],
revisions: [],
invalidSubDir: 'Source1'
}, {
url: 'https://github.com/wcoder/highlightjs-line-numbers.js',
type: 'git',
content: [{
path: 'src/highlightjs-line-numbers.js'
}],
directory: [],
revisions: ['1c480a4573d2a003fc2630c21c2b25829de49972'],
release: {
name: 'v2.6.0',
id: '6877028d6e5412780517d0bfa81f07f6c51abb41',
directory: '5b61d50ef35ca9a4618a3572bde947b8cccf71ad'
}
}];
for (const origin of swhTestsData.origin) {
const metadata = await getMetadataForOrigin(origin.url, config.baseUrl);
const directoryApiUrl = `${config.baseUrl}/api/1/directory/${metadata.directory}`;
origin.dirContent = await httpGet(directoryApiUrl);
origin.rootDirectory = metadata.directory;
origin.revisions.push(metadata.revision);
origin.snapshot = metadata.snapshot;
for (const content of origin.content) {
const contentPathApiUrl = `${config.baseUrl}/api/1/directory/${origin.rootDirectory}/${content.path}`;
const contentMetaData = await httpGet(contentPathApiUrl);
content.name = contentMetaData.name.split('/').slice(-1)[0];
content.sha1git = contentMetaData.target;
content.directory = contentMetaData.dir_id;
const rawFileUrl = `${config.baseUrl}/browse/content/sha1_git:${content.sha1git}/raw/?filename=${content.name}`;
const fileText = await httpGet(rawFileUrl);
const fileLines = fileText.split('\n');
content.numberLines = fileLines.length;
if (!fileLines[content.numberLines - 1]) {
// If last line is empty its not shown
content.numberLines -= 1;
}
}
}
global.swhTestsData = swhTestsData;
}
return global.swhTestsData;
},
'db:user_mailmap:delete': () => {
const db = getDatabase();
db.serialize(function() {
db.run('DELETE FROM user_mailmap');
db.run('DELETE FROM user_mailmap_event');
});
db.close();
return true;
},
'db:user_mailmap:mark_processed': () => {
const db = getDatabase();
db.serialize(function() {
db.run('UPDATE user_mailmap SET mailmap_last_processing_date=datetime("now", "+1 hour")');
});
db.close();
return true;
+ },
+ 'db:add_forge_now:delete': () => {
+ const db = getDatabase();
+ db.serialize(function() {
+ db.run('DELETE FROM add_forge_now_request');
+ });
+ db.close();
+ return true;
}
});
return config;
};
diff --git a/cypress/support/index.js b/cypress/support/index.js
index 1c09614d..e703ec17 100644
--- a/cypress/support/index.js
+++ b/cypress/support/index.js
@@ -1,106 +1,110 @@
/**
* Copyright (C) 2019-2022 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
import 'cypress-hmr-restarter';
import '@cypress/code-coverage/support';
Cypress.Screenshot.defaults({
screenshotOnRunFailure: false
});
Cypress.Commands.add('xhrShouldBeCalled', (alias, timesCalled) => {
const testRoutes = cy.state('routes');
const aliasRoute = Cypress._.find(testRoutes, {alias});
expect(Object.keys(aliasRoute.requests || {})).to.have.length(timesCalled);
});
function loginUser(username, password) {
const url = '/admin/login/';
return cy.request({
url: url,
method: 'GET'
}).then(() => {
cy.getCookie('sessionid').should('not.exist');
cy.getCookie('csrftoken').its('value').then((token) => {
cy.request({
url: url,
method: 'POST',
form: true,
followRedirect: false,
body: {
username: username,
password: password,
csrfmiddlewaretoken: token
}
}).then(() => {
cy.getCookie('sessionid').should('exist');
return cy.getCookie('csrftoken').its('value');
});
});
});
}
Cypress.Commands.add('adminLogin', () => {
return loginUser('admin', 'admin');
});
Cypress.Commands.add('userLogin', () => {
return loginUser('user', 'user');
});
+Cypress.Commands.add('user2Login', () => {
+ return loginUser('user2', 'user2');
+});
+
Cypress.Commands.add('ambassadorLogin', () => {
return loginUser('ambassador', 'ambassador');
});
Cypress.Commands.add('depositLogin', () => {
return loginUser('deposit', 'deposit');
});
Cypress.Commands.add('addForgeModeratorLogin', () => {
return loginUser('add-forge-moderator', 'add-forge-moderator');
});
function mockCostlyRequests() {
cy.intercept('https://status.softwareheritage.org/**', {
body: {
'result': {
'status': [
{
'id': '5f7c4c567f50b304c1e7bd5f',
'name': 'Save Code Now',
'updated': '2020-11-30T13:51:21.151Z',
'status': 'Operational',
'status_code': 100
}
]
}
}}).as('swhPlatformStatus');
cy.intercept('/coverage', {
body: ''
}).as('swhCoverageWidget');
}
Cypress.Commands.add('mailmapAdminLogin', () => {
return loginUser('mailmap-admin', 'mailmap-admin');
});
before(function() {
mockCostlyRequests();
cy.task('getSwhTestsData').then(testsData => {
Object.assign(this, testsData);
});
cy.visit('/').window().then(async win => {
this.Urls = win.Urls;
});
});
beforeEach(function() {
mockCostlyRequests();
});
diff --git a/swh/web/tests/create_test_users.py b/swh/web/tests/create_test_users.py
index 968de489..2ffc8902 100644
--- a/swh/web/tests/create_test_users.py
+++ b/swh/web/tests/create_test_users.py
@@ -1,50 +1,51 @@
# Copyright (C) 2021-2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
from typing import Dict, List, Tuple
from django.contrib.auth import get_user_model
from swh.web.auth.utils import (
ADD_FORGE_MODERATOR_PERMISSION,
ADMIN_LIST_DEPOSIT_PERMISSION,
- SWH_AMBASSADOR_PERMISSION,
MAILMAP_ADMIN_PERMISSION,
+ SWH_AMBASSADOR_PERMISSION,
)
from swh.web.tests.utils import create_django_permission
User = get_user_model()
users: Dict[str, Tuple[str, str, List[str]]] = {
"user": ("user", "user@example.org", []),
+ "user2": ("user2", "user2@example.org", []),
"ambassador": (
"ambassador",
"ambassador@example.org",
[SWH_AMBASSADOR_PERMISSION],
),
"deposit": ("deposit", "deposit@example.org", [ADMIN_LIST_DEPOSIT_PERMISSION]),
"add-forge-moderator": (
"add-forge-moderator",
"moderator@example.org",
[ADD_FORGE_MODERATOR_PERMISSION],
),
"mailmap-admin": (
"mailmap-admin",
"mailmap-admin@example.org",
[MAILMAP_ADMIN_PERMISSION],
),
}
for username, (password, email, permissions) in users.items():
if not User.objects.filter(username=username).exists():
user = User.objects.create_user(username, email, password)
if permissions:
for perm_name in permissions:
permission = create_django_permission(perm_name)
user.user_permissions.add(permission)
user.save()

File Metadata

Mime Type
text/x-diff
Expires
Jun 4 2025, 6:44 PM (14 w, 15 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3272204

Event Timeline