/*
* Copyright 2023 Anyware Services
*
* Licensed 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.
*/
/**
* This tool displays all migrations statuses
*/
Ext.define('Ametys.plugins.admin.migration.MigrationsTool', {
extend: 'Ametys.tool.Tool',
/**
* @property {Ext.grid.Panel} _migrationsGrid The migrations grid
* @private
*/
/**
* @private
* @property {Ext.tree.Panel} _migrationsTree The migrations tree
*/
statics: {
ICONS: {
"plugin": "ametysicon-abecedary4",
"component": "ametysicon-puzzle-piece1",
"container": "ametysicon-puzzle33",
"past-before": "ametysicon-desktop-archive",
"past-notdone": "ametysicon-desktop-archive-black",
"past-done": "ametysicon-arrow-right-curve",
"current": "ametysicon-star129",
"error": "ametysicon-code-misc-bug",
"pending": "ametysicon-datetime-clock"
},
ICONSINTERNAL: {
"plugin": "ametysicon-abecedary4",
"component": "ametysicon-three115",
"container": "ametysicon-puzzle33",
"past-before": "ametysicon-desktop-archive",
"past-notdone": "ametysicon-desktop-archive-black",
"past-done": "ametysicon-arrow-right-curve",
"current": "ametysicon-star129",
"error": "ametysicon-code-misc-bug",
"pending": "ametysicon-datetime-clock"
},
TOGGLED: {
"pending": false,
"error": false,
"current": false,
"past-done": false,
"past-notdone": false,
"past-before": true
},
TIP: {
"pending": "{{i18n PLUGINS_ADMIN_TOOL_MIGRATIONS_FILTER_BUTTON_PENDING}}",
"error": "{{i18n PLUGINS_ADMIN_TOOL_MIGRATIONS_FILTER_BUTTON_ERROR}}",
"current": "{{i18n PLUGINS_ADMIN_TOOL_MIGRATIONS_FILTER_BUTTON_CURRENT}}",
"past-done": "{{i18n PLUGINS_ADMIN_TOOL_MIGRATIONS_FILTER_BUTTON_PASTDONE}}",
"past-notdone": "{{i18n PLUGINS_ADMIN_TOOL_MIGRATIONS_FILTER_BUTTON_PASTNOTDONE}}",
"past-before": "{{i18n PLUGINS_ADMIN_TOOL_MIGRATIONS_FILTER_BUTTON_PASBEFORE}}"
},
},
constructor: function(config)
{
this._filter = Ext.Function.createBuffered(this._filter, 200, this);
Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onMigrationModifiedOrDeleted, this);
Ametys.message.MessageBus.on(Ametys.message.Message.DELETED, this._onMigrationModifiedOrDeleted, this);
this.callParent(arguments);
},
getMBSelectionInteraction: function ()
{
return Ametys.tool.Tool.MB_TYPE_ACTIVE;
},
sendCurrentSelection: function()
{
var selection = this._migrationsTree.getSelection();
var targets = [];
if (selection.length > 0)
{
var node = selection[0];
targets.push(this.getMessageTargetConfiguration(node));
}
Ext.create('Ametys.message.Message', {
type: Ametys.message.Message.SELECTION_CHANGED,
targets: targets
});
},
_onMigrationModifiedOrDeleted: function(message)
{
var me = this;
var targets = message.getTargets(function(t) { return t.getId() == Ametys.message.MessageTarget.MIGRATION || t.getId() == Ametys.message.MessageTarget.MIGRATION_NODE });
for (var i=0; i < targets.length; i++)
{
var target = targets[i];
var index = this._migrationsTree.getStore().findBy(function (r) {
return r.get('componentId') == target.getParameters()['componentId']
&& r.get('internal') == target.getParameters()['internal']
&& r.get('versionListId') == target.getParameters()['versionListId']
&& (target.getId() == Ametys.message.MessageTarget.MIGRATION_NODE || r.get('component') == target.getParameters()['version']);
});
var node = this._migrationsTree.getStore().getAt(index);
var selectPath = node.getPath('component');
var parentNode = target.getId() == Ametys.message.MessageTarget.MIGRATION_NODE ? node : node.parentNode;
this._migrationsTree.getStore().load({
node: parentNode,
callback: function(records, operation, success) {
if (success)
{
me._migrationsTree.selectPath(selectPath, "component");
var parentNodeToUpdate = parentNode;
var infosToUpdateOnParents = JSON.parse(operation.getResponse().innerHTML).parentInfos
for (var i = infosToUpdateOnParents.length - 1; i >= 0; i--)
{
var infosToUpdateOnParent = infosToUpdateOnParents[i];
Object.keys(infosToUpdateOnParent).forEach(function(k){
parentNodeToUpdate.set(k, infosToUpdateOnParent[k]);
});
parentNodeToUpdate = parentNodeToUpdate.parentNode;
}
}
}
});
}
},
/**
* @private
* Get the target configuration object for given record
* @param {Ext.data.Model} record The tree record to convert to its Ametys.message.MessageTarget configuration
* @return {Object} The configuration to create a Ametys.message.MessageTarget. Can be null, if the record is null or not relevant to be a messagetarget.
*/
getMessageTargetConfiguration: function (record)
{
if (record == null)
{
// Empty selection
return null;
}
else
{
if (record.get('children') == null)
{
return {
id: Ametys.message.MessageTarget.MIGRATION,
parameters: {
componentId: record.get('componentId'),
internal: record.get('internal'),
versionListId: record.get('versionListId'),
version: record.get('component'),
type: record.get('type'),
comment: record.get('comment')
}
};
}
else
{
var params = {
type: record.get('type'),
};
if (record.get('versionListId'))
{
params.versionListId = record.get('versionListId');
}
if (record.get('internal') !== undefined)
{
params.internal = record.get('internal');
}
if (record.get('componentId'))
{
params.componentId = record.get('componentId');
}
return {
id: Ametys.message.MessageTarget.MIGRATION_NODE,
parameters: params
};
}
}
},
createPanel: function ()
{
this._createMigrationsTreePanel();
return this._migrationsTree;
},
setParams: function (params)
{
this.callParent(arguments);
this.showOutOfDate();
},
refresh: function ()
{
this.showRefreshing();
this._migrationsTree.getStore().load({
callback: function(){
this._migrationsTree.getRootNode().expand();
var errorRecordIndex = this._migrationsTree.getStore().findBy(function(record) {
return record.get('type') == 'error'
|| record.get('type') == 'current' && record.get('failed') == true;
});
if (errorRecordIndex >= 0)
{
var errorRecord = Ametys.tool.ToolsManager.getFocusedTool()._migrationsTree.getStore().getAt(errorRecordIndex);
// autoselect / scroll to error if any
this._migrationsTree.getSelectionModel().select([errorRecord]);
this._migrationsTree.ensureVisible(errorRecord.getPath());
}
this.showRefreshed();
},
scope: this
});
},
_createMigrationsTreePanel: function ()
{
var me = this;
var filterbar = {
dock: 'top',
xtype: 'toolbar',
layout: {
type: 'hbox',
align: 'stretch'
},
border: false,
defaults : {
cls: 'ametys',
labelWidth: 55,
labelSeparator: ''
},
items: [{
// Search input
xtype: 'textfield',
itemId: 'search-filter-input',
cls: 'ametys',
flex: 1,
maxWidth: 300,
emptyText: "{{i18n PLUGINS_ADMIN_TOOL_MIGRATIONS_FILTER_INPUT}}",
enableKeyEvents: true,
msgTarget: 'qtip',
margin: '0 0 0 0',
listeners: {change: Ext.bind(this._filter, this)}
}, {
// Clear filter
tooltip: "{{i18n PLUGINS_ADMIN_TOOL_MIGRATIONS_FILTER_CLEAR}}",
handler: Ext.bind (this._clearFilter, this),
iconCls: 'a-btn-glyph ametysicon-eraser11 size-16',
margin: '0 0 0 0',
cls: 'a-btn-light'
},
'', {
// Expand all
tooltip: "{{i18n PLUGINS_ADMIN_TOOL_MIGRATIONS_FILTER_EXPANDALL}}",
handler: Ext.bind (this._expandAll, this),
iconCls: 'a-btn-glyph ametysicon-sign-add size-16',
margin: '0 0 0 0',
cls: 'a-btn-light'
}, {
// Collapse all
tooltip: "{{i18n PLUGINS_ADMIN_TOOL_MIGRATIONS_FILTER_COLLAPSEALL}}",
handler: Ext.bind (this._collapseAll, this),
iconCls: 'a-btn-glyph ametysicon-sign-minus size-16',
margin: '0 0 0 0',
cls: 'a-btn-light'
},
'']
};
Object.keys(Ametys.plugins.admin.migration.MigrationsTool.TOGGLED).forEach(function (v){
filterbar.items.push({
iconCls: Ametys.plugins.admin.migration.MigrationsTool.ICONS[v],
enableToggle: true,
itemId: 'search-filter-button-type-' + v,
pressed: Ametys.plugins.admin.migration.MigrationsTool.TOGGLED[v],
tooltip: Ametys.plugins.admin.migration.MigrationsTool.TIP[v],
margin: '0 0 0 0',
cls: 'a-btn-light',
toggleHandler: function(btn, state) {
Ametys.plugins.admin.migration.MigrationsTool.TOGGLED[v] = state;
me._filter();
},
listeners: {
dblclick: {
element: 'el',
fn: function() {
Object.keys(Ametys.plugins.admin.migration.MigrationsTool.TOGGLED).forEach(function (v2){
if (v2 != v)
{
me._migrationsTree.down("#search-filter-button-type-" + v2).toggle(true);
}
else
{
me._migrationsTree.down("#search-filter-button-type-" + v2).toggle(false);
}
});
}
}
}
});
});
this._migrationsTree = Ext.create('Ext.tree.Panel', {
cls:'uitool-admin-migrations',
border: false,
scrollable: true,
// stateful: true,
stateId: this.self.getName() + "$grid",
rootVisible: false,
plugins: ['gridfilters'],
viewConfig: {
getRowClass: function(record) {
return record.get('failed') ? 'error' :
(record.get('warning') ? 'warning' :
(record.get('type') == 'past-before' ? 'past' :
(record.get('type') == 'past-notdone' ? 'past' :
'')));
}
},
columns: [
{
xtype: 'treecolumn',
stateId: 'grid-component',
text: "{{i18n PLUGINS_ADMIN_TOOL_MIGRATIONS_COL_COMPONENT}}",
dataIndex: 'component',
sortable: false,
width: 500
},
{
stateId: 'grid-instant',
text: "{{i18n PLUGINS_ADMIN_TOOL_MIGRATIONS_COL_DATE}}",
dataIndex: 'instant',
format: Ext.Date.patterns.ShortDateTime,
xtype: 'datecolumn',
width: 125,
sortable: false
},
{
stateId: 'grid-comment',
text: "{{i18n PLUGINS_ADMIN_TOOL_MIGRATIONS_COL_COMMENT}}",
dataIndex: 'comment',
sortable: false,
flex: 1,
renderer: function(value, metadata, record, rowIndex, colIndex, store, view) {
var comment = record.get('comment') || "";
var errorComment = record.get('errorComment') || "";
if (errorComment)
{
errorComment = "<i>" + errorComment + "</i>";
}
function _escape(t)
{
return (t || "").replaceAll(/(http(?:s)?:\/\/(?:\S)+)/g, "<a target='_blank' href='$1'>$1</a>")
.replaceAll(/([A-Z]{2,10}-[0-9]{1,6})/g, "<a target='_blank' href='https://issues.ametys.org/browse/$1'>$1</a>");
}
return _escape(comment) + (comment && errorComment ? "<br/>" : "") + _escape(errorComment);
}
}
],
// Filter box
dockedItems: [filterbar],
store: Ext.create('Ext.data.TreeStore', {
model: 'Ametys.plugins.admin.migration.MigrationsTool.Migration',
root: {
component: 'root',
id: 'root',
expanded: false
},
autoLoad: false,
proxy: {
type: 'ametys',
role: 'org.ametys.runtime.plugins.admin.migration.MigrationsStatus',
methodName: 'getMigrationsStatus',
methodArguments: null,
reader: {
type: 'json'
}
},
listeners: {
beforeload: this._onBeforeLoad,
scope: this
}
}),
listeners: {
'selectionchange': Ext.bind(this.sendCurrentSelection, this),
}
});
this._filter();
return this._migrationsTree;
},
/**
* @private
* Appends the path parameter to store's Ajax requests.
*/
_onBeforeLoad: function(store, operation, eOpts)
{
//Passing 'path' as extra parameter during the 'node expand' Ajax call
operation.setParams( Ext.apply(operation.getParams(), {
path: operation.node.getPath('component'),
}));
},
/**
* @private
* Expand all visible nodes
*/
_expandAll: function()
{
var me = this;
this._migrationsTree.getRootNode().cascade(function(record) {
if (record.get('visible') != false)
{
me._migrationsTree.expandNode(record);
}
});
},
/**
* @private
* Collapse all nodes except plugins
*/
_collapseAll: function()
{
var me = this;
this._migrationsTree.getRootNode().cascade(function(record) {
if (id == 'root' || record.get('type') == 'plugin')
{
me._migrationsTree.expandNode(record);
}
else if (record.get('type') == 'component')
{
me._migrationsTree.collapseNode(record);
}
});
},
/**
* @private
* Filters queries by input field value.
*/
_filter: function ()
{
var currentSelection = this._migrationsTree.getSelection();
var value = Ext.String.trim(this._migrationsTree.down("#search-filter-input").getValue());
this._regexFilter = value ? new RegExp(value, 'i') : null;
this._migrationsTree.getStore().removeFilter();
this._migrationsTree.getStore().addFilter({
filterFn: Ext.bind(this._filterByTextAndChildren, this)
});
if (currentSelection.length > 0)
{
var me = this;
Ext.Array.each(currentSelection, function (sel) {
if (me._migrationsTree.getStore().findExact(me._migrationsTree.getStore().getModel().idProperty, sel.getId()) == -1)
{
// The current selection is not visible, clear the selection
me._migrationsTree.getSelectionModel().deselect([sel]);
}
});
var selection = this._migrationsTree.getSelection();
if (selection.length > 0)
{
this._migrationsTree.ensureVisible(selection[0].getPath());
}
}
},
/**
* @private
* Filter function that check if a node in the tree should be visible or not.
* @param {Ext.data.Model} record The record to check.
* @return {boolean} True if the record should be visible.
*/
_filterByTextAndChildren: function (record)
{
var toggleVisible = true;
Object.keys(Ametys.plugins.admin.migration.MigrationsTool.TOGGLED).forEach(function (v){
if (toggleVisible
&& Ametys.plugins.admin.migration.MigrationsTool.TOGGLED[v]
&& record.get("type") == v)
{
toggleVisible = false;
}
});
var isVisible = toggleVisible
&& (this._regexFilter == null
|| this._regexFilter.test(record.data.component)
|| this._regexFilter.test(record.data.comment));
if (!isVisible)
{
// if the record does not match, we check if any child is visible. If at least one is, this record should not be hidden.
// This is efficient because the data is in the store, and is not loaded in the view.
for (var i = 0; !isVisible && i < record.childNodes.length; i++)
{
isVisible = this._filterByTextAndChildren(record.childNodes[i]);
}
}
else if (this._regexFilter == null
&& record.childNodes.length > 0)
{
// When no textual search and no visible childnode... finally not visible
var anyVisible = false;
for (var i = 0; !anyVisible && i < record.childNodes.length; i++)
{
anyVisible = this._filterByTextAndChildren(record.childNodes[i]);
}
if (!anyVisible)
{
isVisible = false;
}
}
if (isVisible && !this._regexFilter == null)
{
this._migrationsTree.expandNode(record);
}
return isVisible;
},
/**
* Clear the current filter
*/
_clearFilter: function()
{
this._migrationsTree.down("#search-filter-input").reset();
}
});
Ext.define('Ametys.plugins.admin.migration.MigrationsTool.Migration', {
extend: 'Ext.data.TreeModel',
fields: [
{ name: 'id' },
{ name: 'component' },
{ name: 'componentId' },
{ name: 'versionListId' },
{ name: 'comment' },
{ name: 'errorComment' },
{ name: 'failed', type: 'boolean' },
{ name: 'internal', type: 'boolean' },
{ name: 'warning', type: 'boolean' },
{ name: 'instant', type: 'date' },
{ name: 'type' },
{
name: 'iconCls',
calculate: function(data)
{
return Ametys.plugins.admin.migration.MigrationsTool['ICONS' + (data.internal === true ? 'INTERNAL' : '')][data.type];
}
},
{
name: 'expanded',
type: 'boolean',
mapping: function(data) {
return (data.expanded == true) || (data.failed == true);
}
},
{ name: 'current', type: 'boolean' },
{ name: 'children' },
{
name: 'leaf',
type: 'boolean',
depends: ['children', 'component'],
convert: function (value, record) {
return record.data.id != 'root' && (!record.data.children || record.data.children.length == 0);
}
}
]
});
Ext.define("Ametys.message.AdminMigrationsMessageTarget",
{
override: "Ametys.message.MessageTarget",
statics:
{
/**
* @member Ametys.message.MessageTarget
* @readonly
* @property {String} MIGRATION The target of the message is an Ametys migration
* @property {String} MIGRATION.componentId The id of the migration component
* @property {String} MIGRATION.versionListId The id of the version list
* @property {String} MIGRATION.version The version number
* @property {Boolean} MIGRATION.internal Is internal
* @property {String} MIGRATION.comment The version comment
*/
MIGRATION: "migration",
/**
* @member Ametys.message.MessageTarget
* @readonly
* @property {String} MIGRATION_NODE The target of the message is an Ametys migration wrapper (plugin, component or container)
* @property {String} [MIGRATION.componentId] The id of the migration component if relevant
* @property {Boolean} [MIGRATION.internal] Is internal if relevant
* @property {String} [MIGRATION.versionListId] The id of the version list if relevant
*/
MIGRATION_NODE: "migration-node"
}
}
);