Skip to content

Commit

Permalink
[OGUI-1556] Allow modification of existing layouts via JSON panel (#2629
Browse files Browse the repository at this point in the history
)

* [OGUI-1556] allow modification of existing layouts via JSON file
* Adds a new component modal that loads the layout structure in a JSON format
* Allows users to change the structure in the JSON format making it more flexible
---------

Co-authored-by: George Raduta <george.raduta@cern.ch>
  • Loading branch information
mariscalromeroalejandro and graduta authored Nov 21, 2024
1 parent 3d489be commit c855b8c
Show file tree
Hide file tree
Showing 12 changed files with 491 additions and 150 deletions.
19 changes: 19 additions & 0 deletions QualityControl/public/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export default class Model extends Observable {
this.accountMenuEnabled = false;
this.page = null;
this._isImportVisible = false; // Visibility of modal allowing user to import a layout as JSON
this._isUpdateVisible = false; // Visibility of modal allowing user to edit JSON of an existing layout

// Setup router
this.router = new QueryRouter();
Expand Down Expand Up @@ -374,4 +375,22 @@ export default class Model extends Observable {
this._isImportVisible = value ? true : false;
this.notify();
}

/**
* Returns the visibility of the edit JSON layout modal
* @returns {boolean} - whether import modal is visible
*/
get isUpdateVisible() {
return this._isUpdateVisible;
}

/**
* Sets the visibility of the edit JSON layout modal
* @param {boolean} value - value to be set for modal visibility
* @returns {undefined}
*/
set isUpdateVisible(value) {
this._isUpdateVisible = value ? true : false;
this.notify();
}
}
3 changes: 3 additions & 0 deletions QualityControl/public/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,6 @@

.o2-modal{z-index:1001; display:block; padding-top:10em; position:fixed; left:0; top:0; width:100%; height:100%; overflow:auto; background-color:rgb(0,0,0); background-color:rgba(0,0,0,0.4); }
.o2-modal-content{margin:auto; background-color:#fff; position:relative; padding:0; outline:0; width:700px; border-radius: 5px 20px 5px; }

.right-menu {right:0; left:auto;}
.resize-vertical { resize: vertical}
81 changes: 81 additions & 0 deletions QualityControl/public/layout/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export default class Layout extends Observable {
this.tabInterval = undefined; // JS Interval to change currently displayed tab

this.newJSON = undefined;
this.updatedJSON = undefined;

this.requestedLayout = RemoteData.notAsked();

Expand All @@ -52,6 +53,8 @@ export default class Layout extends Observable {
this.editingTabObject = null; // Pointer to a tabObject being modified
this.editOriginalClone = null; // Contains a deep clone of item before editing

this.isEditLayoutDropdownOpen = false;

// https://github.com/hootsuite/grid
this.gridListSize = 3;

Expand Down Expand Up @@ -313,6 +316,15 @@ export default class Layout extends Observable {
this.model.notify();
}

/**
* Toggle edit menu dropdown
* @returns {undefined}
*/
async toggleEditMenu() {
this.isEditLayoutDropdownOpen = !this.isEditLayoutDropdownOpen;
this.notify();
}

/**
* Method to allow more than 3x3 grid
* @param {string} value - of grid resize
Expand Down Expand Up @@ -450,6 +462,7 @@ export default class Layout extends Observable {
* @returns {undefined}
*/
edit() {
this.toggleEditMenu();
this.model.services.object.listObjects();
if (!this.item) {
throw new Error('An item should be loaded before editing it');
Expand Down Expand Up @@ -727,4 +740,72 @@ export default class Layout extends Observable {
this.selectTab(this._tabIndex);
}
}

/**
* Validates the provided layout and updates the layout state accordingly.
* Used by the textarea input to check the JSON structure on each input change.
* @param {string} newLayout - The layout to check.
* @returns {undefined}
*/
checkLayoutToUpdate(newLayout) {
try {
const newJSON = JSON.parse(newLayout);
this.checkForManualIdEntry(newJSON);
this.model.services.layout.update = RemoteData.success();
} catch (error) {
this.model.services.layout.update = RemoteData.failure(error.message || error);
}
this.updatedJSON = newLayout;
this.notify();
}

/**
* Checks that user doesn't enter the ID
* @param {object} layoutJSON - layout entered by the user in the box
* @returns {undefined}
*/
checkForManualIdEntry(layoutJSON) {
if (Object.keys(layoutJSON).includes('id')) {
throw new Error('Error: Manual entry of an ID is not allowed, as it is automatically assigned by the system.');
}
}

/**
* Updates the layout by parsing the updated JSON and saving the layout state.
* @returns {undefined}
*/
updateLayout() {
try {
const updatedLayout = LayoutUtils.fromSkeleton({
...this.item,
...JSON.parse(this.updatedJSON),
});

this.item = {
...updatedLayout,
id: this.item.id,
};

this.save();
this.updatedJSON = undefined;
this.model.isUpdateVisible = !this.model.isUpdateVisible;
} catch (error) {
this.changeUpdateStatus(RemoteData.failure(error.message || error));
}
this.notify();
}

/**
* Method to initialize the status of the EDIT as JSON modal
* Sets the layout skeleton to the current layout
* Sets the error message to null
* Sets the visibility of the model to true
* @returns {undefined}
*/
initializeEditViaJson() {
this.model.services.layout.update = RemoteData.success();
this.updatedJSON = LayoutUtils.toSkeleton(this.item);
this.model.isUpdateVisible = true;
this.toggleEditMenu();
}
}
53 changes: 53 additions & 0 deletions QualityControl/public/layout/panels/editModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* @license
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/

import { h } from '/js/src/index.js';

/**
* Displays a panel allowing users to edit the JSON file of the layout
* @param {Model} model - root model of the application
* @returns {vnode} - virtual node element
*/
export default (model) => h('.o2-modal', [
h('.o2-modal-content', [
h('.p2.text-center.flex-column', [
h('h4.pv1', 'Edit JSON file of a layout'),
h('', h('textarea.form-control.w-100.resize-vertical', {
rows: 15,
oninput: (e) => model.layout.checkLayoutToUpdate(e.target.value),
id: 'layout-json-editor',
value: model.layout.updatedJSON,
})),
model.services.layout.update.match({
NotAsked: () => null,
Loading: () => h('', 'Loading...'),
Success: (_) => null,
Failure: (error) => h('.danger.pv1', error),
}),
h('.btn-group.w-100.align-center.pv1', {
style: 'display:flex; justify-content:center;',
}, [
h('button.btn.btn-primary', {
disabled: model.services.layout.update.isFailure(),
onclick: () => model.layout.updateLayout(),
}, 'Update layout'),
h('button.btn', {
onclick: () => {
model.isUpdateVisible = false;
},
}, 'Cancel'),
]),
]),
]),
]);
3 changes: 1 addition & 2 deletions QualityControl/public/layout/panels/importModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,11 @@ export default (model) =>
h('.o2-modal-content', [
h('.p2.text-center.flex-column', [
h('h4.pv1', 'Import a layout in JSON format'),
h('', h('textarea.form-control.w-100', {
h('', h('textarea.form-control.w-100.resize-vertical', {
rows: 15,
placeholder: 'e.g.\n{\n\t"name": "my layout",\n\t"displayTimestamp": "false",' +
'\n\t"displayTimestamp": "autoTabChange": "10", \n\t"tabs": "[]"\n}',
oninput: (e) => model.layout.setImportValue(e.target.value),
style: 'resize: vertical;',
})),
model.services.layout.new.match({
NotAsked: () => null,
Expand Down
17 changes: 14 additions & 3 deletions QualityControl/public/layout/view/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,21 @@ const toolbarViewMode = (model) => {
download: `layout-${layoutItem.name}-skeleton.json`,
}, iconShareBoxed()),
model.session.personid == layoutItem.owner_id && [
h('button.btn.btn-primary', {
onclick: () => model.layout.edit(),
h('.dropdown', {
title: 'Edit layout',
}, iconPencil()),
class: model.layout.isEditLayoutDropdownOpen ? 'dropdown-open' : '',
}, [
h('button.btn.btn-primary', { onclick: () => model.layout.toggleEditMenu() }, iconPencil()),
h('.dropdown-menu.right-menu', [
h('.text-ellipsis', [
h('a.menu-item', { title: 'Edit via GUI', onclick: () => model.layout.edit() }, 'Edit via GUI'),
h('a.menu-item', {
title: 'Edit via JSON',
onclick: () => model.layout.initializeEditViaJson(),
}, 'Edit via JSON'),
]),
]),
]),
h('button.btn.btn-danger', {
onclick: () => confirm('Are you sure to delete this layout?') && model.layout.deleteItem(),
title: 'Delete layout',
Expand Down
1 change: 1 addition & 0 deletions QualityControl/public/services/Layout.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default class LayoutService {
this.loader = model.loader;

this.new = RemoteData.notAsked(); // RemoteData for creating a new layout via modal of import or prompt
this.update = RemoteData.notAsked(); // RemoteData for updating the JSON file that builds the layout

this.list = RemoteData.notAsked(); // List of all existing layouts in QCG;
this.userList = RemoteData.notAsked(); // List of layouts owned by current user;
Expand Down
3 changes: 3 additions & 0 deletions QualityControl/public/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import header from './common/header.js';
import layoutListPage from './layout/list/page.js';
import layoutViewPage from './layout/view/page.js';
import layoutImportModal from './layout/panels/importModal.js';
import layoutEditModal from './layout/panels/editModal.js';

import objectTreePage from './object/objectTreePage.js';
import ObjectViewPage from './pages/objectView/ObjectViewPage.js';
import frameworkInfoPage from './frameworkInfo/frameworkInfoPage.js';
Expand All @@ -30,6 +32,7 @@ import frameworkInfoPage from './frameworkInfo/frameworkInfoPage.js';
* @returns {vnode} - virtual node element
*/
export default (model) => [
model.isUpdateVisible && layoutEditModal(model),
model.isImportVisible && layoutImportModal(model),
model.page === 'objectView' ? ObjectViewPage(model) :
h('.absolute-fill.flex-column', [
Expand Down
Loading

0 comments on commit c855b8c

Please sign in to comment.