mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-10 09:07:54 +01:00
Add ability to quickly create exceptions in logger
This is a feature under development, hidden behind a new advanced setting, `filterAuthorMode` which default to `false`. Ability to point-and-click to create temporary exception filters for static extended filters (i.e. cosmetic, scriptlet & html filters) from within the summary pane in the logger. The button to toggle on/off temporary exception filter is labeled `#@#`. The created exceptions are temporary and will be lost when restarting uBO, or manually toggling off the exception filters. Creating temporary exception filters does not cause the filter lists to reloaded, and thus there is no overhead in creating/removing these temporary exception filters.
This commit is contained in:
parent
733b2330de
commit
59c9a34d34
9 changed files with 203 additions and 47 deletions
|
@ -660,6 +660,7 @@ body[dir="rtl"] #netFilteringDialog > .panes > .details > div > span:nth-of-type
|
|||
border-left: 1px solid white;
|
||||
}
|
||||
#netFilteringDialog > .panes > .details > div > span:nth-of-type(2) {
|
||||
flex-grow: 1;
|
||||
max-height: 20vh;
|
||||
overflow: hidden auto;
|
||||
white-space: pre-line
|
||||
|
@ -675,6 +676,26 @@ body[dir="rtl"] #netFilteringDialog > .panes > .details > div > span:nth-of-type
|
|||
#netFilteringDialog > .panes > .details > div > span:nth-of-type(2) .fa-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
#netFilteringDialog > .panes > .details .exceptor {
|
||||
align-items: center;
|
||||
border-left: 1px solid white;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
font-family: monospace;
|
||||
opacity: 0.8;
|
||||
}
|
||||
#netFilteringDialog > .panes > .details .exceptor:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
#netFilteringDialog > .panes > .details .exceptored .filter {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
#netFilteringDialog > .panes > .details .exceptored .exceptor {
|
||||
background-color: lightblue;
|
||||
}
|
||||
#netFilteringDialog > .panes > .details .exceptor::before {
|
||||
content: '#@#';
|
||||
}
|
||||
#netFilteringDialog > div.panes > .dynamic > .toolbar {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ const µBlock = (( ) => { // jshint ignore:line
|
|||
extensionUpdateForceReload: false,
|
||||
ignoreRedirectFilters: false,
|
||||
ignoreScriptInjectFilters: false,
|
||||
filterAuthorMode: false,
|
||||
loggerPopupType: 'popup',
|
||||
manualUpdateAssetFetchPeriod: 500,
|
||||
popupFontSize: 'unset',
|
||||
|
|
|
@ -826,6 +826,12 @@ FilterContainer.prototype.randomAlphaToken = function() {
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
FilterContainer.prototype.getSession = function() {
|
||||
return this.specificFilters.session;
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
FilterContainer.prototype.retrieveGenericSelectors = function(request) {
|
||||
if ( this.acceptedCount === 0 ) { return; }
|
||||
if ( !request.ids && !request.classes ) { return; }
|
||||
|
@ -990,12 +996,15 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
|
|||
}
|
||||
}
|
||||
|
||||
// Retrieve temporary filters
|
||||
this.specificFilters.session.retrieve([ dummySet, exceptionSet ]);
|
||||
|
||||
// Retrieve filters with a non-empty hostname
|
||||
this.specificFilters.retrieve(
|
||||
hostname,
|
||||
options.noSpecificCosmeticFiltering !== true
|
||||
? [ specificSet, exceptionSet, proceduralSet, exceptionSet ]
|
||||
: [ dummySet, exceptionSet, dummySet, exceptionSet ],
|
||||
: [ dummySet, exceptionSet ],
|
||||
1
|
||||
);
|
||||
// Retrieve filters with an empty hostname
|
||||
|
@ -1003,7 +1012,7 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
|
|||
hostname,
|
||||
options.noGenericCosmeticFiltering !== true
|
||||
? [ specificSet, exceptionSet, proceduralSet, exceptionSet ]
|
||||
: [ dummySet, exceptionSet, dummySet, exceptionSet ],
|
||||
: [ dummySet, exceptionSet ],
|
||||
2
|
||||
);
|
||||
// Retrieve filters with a non-empty entity
|
||||
|
@ -1012,7 +1021,7 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
|
|||
`${hostname.slice(0, -request.domain.length)}${request.entity}`,
|
||||
options.noSpecificCosmeticFiltering !== true
|
||||
? [ specificSet, exceptionSet, proceduralSet, exceptionSet ]
|
||||
: [ dummySet, exceptionSet, dummySet, exceptionSet ],
|
||||
: [ dummySet, exceptionSet ],
|
||||
1
|
||||
);
|
||||
}
|
||||
|
|
|
@ -334,6 +334,10 @@
|
|||
}
|
||||
};
|
||||
|
||||
api.getSession = function() {
|
||||
return filterDB.session;
|
||||
};
|
||||
|
||||
api.retrieve = function(details) {
|
||||
const hostname = details.hostname;
|
||||
|
||||
|
@ -350,6 +354,7 @@
|
|||
const procedurals = new Set();
|
||||
const exceptions = new Set();
|
||||
|
||||
filterDB.session.retrieve([ new Set(), exceptions ]);
|
||||
filterDB.retrieve(
|
||||
hostname,
|
||||
[ plains, exceptions, procedurals, exceptions ]
|
||||
|
|
|
@ -41,6 +41,7 @@ let filteredLoggerEntryVoidedCount = 0;
|
|||
let popupLoggerBox;
|
||||
let popupLoggerTooltips;
|
||||
let activeTabId = 0;
|
||||
let filterAuthorMode = false;
|
||||
let selectedTabId = 0;
|
||||
let netInspectorPaused = false;
|
||||
|
||||
|
@ -64,7 +65,7 @@ const tabIdFromAttribute = function(elem) {
|
|||
|
||||
// Current design allows for only one modal DOM-based dialog at any given time.
|
||||
//
|
||||
const modalDialog = (function() {
|
||||
const modalDialog = (( ) => {
|
||||
const overlay = uDom.nodeFromId('modalOverlay');
|
||||
const container = overlay.querySelector(
|
||||
':scope > div > div:nth-of-type(1)'
|
||||
|
@ -949,6 +950,8 @@ const onLogBufferRead = function(response) {
|
|||
allTabIdsToken = response.tabIdsToken;
|
||||
}
|
||||
|
||||
filterAuthorMode = response.filterAuthorMode === true;
|
||||
|
||||
if ( activeTabIdChanged ) {
|
||||
pageSelectorFromURLHash();
|
||||
}
|
||||
|
@ -1085,7 +1088,7 @@ const reloadTab = function(ev) {
|
|||
/******************************************************************************/
|
||||
/******************************************************************************/
|
||||
|
||||
(function() {
|
||||
(( ) => {
|
||||
const reRFC3986 = /^([^:\/?#]+:)?(\/\/[^\/?#]*)?([^?#]*)(\?[^#]*)?(#.*)?/;
|
||||
const reSchemeOnly = /^[\w-]+:$/;
|
||||
const staticFilterTypes = {
|
||||
|
@ -1203,24 +1206,35 @@ const reloadTab = function(ev) {
|
|||
);
|
||||
};
|
||||
|
||||
const onClick = function(ev) {
|
||||
const onClick = async function(ev) {
|
||||
const target = ev.target;
|
||||
const tcl = target.classList;
|
||||
|
||||
// Select a mode
|
||||
if ( tcl.contains('header') ) {
|
||||
dialog.setAttribute('data-pane', target.getAttribute('data-pane') );
|
||||
ev.stopPropagation();
|
||||
dialog.setAttribute('data-pane', target.getAttribute('data-pane') );
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle temporary exception filter
|
||||
if ( tcl.contains('exceptor') ) {
|
||||
ev.stopPropagation();
|
||||
const status = await messaging.send('loggerUI', {
|
||||
what: 'toggleTemporaryException',
|
||||
filter: filterFromTargetRow(),
|
||||
});
|
||||
const row = target.closest('div');
|
||||
row.classList.toggle('exceptored', status);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create static filter
|
||||
if ( target.id === 'createStaticFilter' ) {
|
||||
ev.stopPropagation();
|
||||
const value = staticFilterNode().value;
|
||||
// Avoid duplicates
|
||||
if ( createdStaticFilters.hasOwnProperty(value) ) {
|
||||
return;
|
||||
}
|
||||
if ( createdStaticFilters.hasOwnProperty(value) ) { return; }
|
||||
createdStaticFilters[value] = true;
|
||||
if ( value !== '' ) {
|
||||
messaging.send('loggerUI', {
|
||||
|
@ -1232,21 +1246,19 @@ const reloadTab = function(ev) {
|
|||
});
|
||||
}
|
||||
updateWidgets();
|
||||
ev.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Save url filtering rule(s)
|
||||
if ( target.id === 'saveRules' ) {
|
||||
messaging.send('loggerUI', {
|
||||
ev.stopPropagation();
|
||||
await messaging.send('loggerUI', {
|
||||
what: 'saveURLFilteringRules',
|
||||
context: selectValue('select.dynamic.origin'),
|
||||
urls: targetURLs,
|
||||
type: uglyTypeFromSelector('dynamic'),
|
||||
}).then(( ) => {
|
||||
colorize();
|
||||
});
|
||||
ev.stopPropagation();
|
||||
colorize();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1254,87 +1266,83 @@ const reloadTab = function(ev) {
|
|||
|
||||
// Remove url filtering rule
|
||||
if ( tcl.contains('action') ) {
|
||||
messaging.send('loggerUI', {
|
||||
ev.stopPropagation();
|
||||
await messaging.send('loggerUI', {
|
||||
what: 'setURLFilteringRule',
|
||||
context: selectValue('select.dynamic.origin'),
|
||||
url: target.getAttribute('data-url'),
|
||||
type: uglyTypeFromSelector('dynamic'),
|
||||
action: 0,
|
||||
persist: persist,
|
||||
}).then(( ) => {
|
||||
colorize();
|
||||
});
|
||||
ev.stopPropagation();
|
||||
colorize();
|
||||
return;
|
||||
}
|
||||
|
||||
// add "allow" url filtering rule
|
||||
if ( tcl.contains('allow') ) {
|
||||
messaging.send('loggerUI', {
|
||||
ev.stopPropagation();
|
||||
await messaging.send('loggerUI', {
|
||||
what: 'setURLFilteringRule',
|
||||
context: selectValue('select.dynamic.origin'),
|
||||
url: target.parentNode.getAttribute('data-url'),
|
||||
type: uglyTypeFromSelector('dynamic'),
|
||||
action: 2,
|
||||
persist: persist,
|
||||
}).then(( ) => {
|
||||
colorize();
|
||||
});
|
||||
ev.stopPropagation();
|
||||
colorize();
|
||||
return;
|
||||
}
|
||||
|
||||
// add "block" url filtering rule
|
||||
if ( tcl.contains('noop') ) {
|
||||
messaging.send('loggerUI', {
|
||||
ev.stopPropagation();
|
||||
await messaging.send('loggerUI', {
|
||||
what: 'setURLFilteringRule',
|
||||
context: selectValue('select.dynamic.origin'),
|
||||
url: target.parentNode.getAttribute('data-url'),
|
||||
type: uglyTypeFromSelector('dynamic'),
|
||||
action: 3,
|
||||
persist: persist,
|
||||
}).then(( ) => {
|
||||
colorize();
|
||||
});
|
||||
ev.stopPropagation();
|
||||
colorize();
|
||||
return;
|
||||
}
|
||||
|
||||
// add "block" url filtering rule
|
||||
if ( tcl.contains('block') ) {
|
||||
messaging.send('loggerUI', {
|
||||
ev.stopPropagation();
|
||||
await messaging.send('loggerUI', {
|
||||
what: 'setURLFilteringRule',
|
||||
context: selectValue('select.dynamic.origin'),
|
||||
url: target.parentNode.getAttribute('data-url'),
|
||||
type: uglyTypeFromSelector('dynamic'),
|
||||
action: 1,
|
||||
persist: persist,
|
||||
}).then(( ) => {
|
||||
colorize();
|
||||
});
|
||||
ev.stopPropagation();
|
||||
colorize();
|
||||
return;
|
||||
}
|
||||
|
||||
// Force a reload of the tab
|
||||
if ( tcl.contains('reload') ) {
|
||||
ev.stopPropagation();
|
||||
messaging.send('loggerUI', {
|
||||
what: 'reloadTab',
|
||||
tabId: targetTabId,
|
||||
});
|
||||
ev.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Hightlight corresponding element in target web page
|
||||
if ( tcl.contains('picker') ) {
|
||||
ev.stopPropagation();
|
||||
messaging.send('loggerUI', {
|
||||
what: 'launchElementPicker',
|
||||
tabId: targetTabId,
|
||||
targetURL: 'img\t' + targetURLs[0],
|
||||
select: true,
|
||||
});
|
||||
ev.stopPropagation();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
@ -1426,6 +1434,37 @@ const reloadTab = function(ev) {
|
|||
return urls;
|
||||
};
|
||||
|
||||
const filterFromTargetRow = function() {
|
||||
return targetRow.children[1].textContent;
|
||||
};
|
||||
|
||||
const toSummaryPaneFilterNode = async function(receiver, filter) {
|
||||
receiver.children[1].textContent = filter;
|
||||
if ( filterAuthorMode !== true ) { return; }
|
||||
const match = /#@?#/.exec(filter);
|
||||
if ( match === null ) { return; }
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment.appendChild(document.createTextNode(match[0]));
|
||||
const selector = filter.slice(match.index + match[0].length);
|
||||
const span = document.createElement('span');
|
||||
span.className = 'filter';
|
||||
span.textContent = selector;
|
||||
fragment.appendChild(span);
|
||||
let isTemporaryException = false;
|
||||
if ( match[0] === '#@#' ) {
|
||||
isTemporaryException = await messaging.send('loggerUI', {
|
||||
what: 'hasTemporaryException',
|
||||
filter,
|
||||
});
|
||||
receiver.classList.toggle('exceptored', isTemporaryException);
|
||||
}
|
||||
if ( match[0] === '##' || isTemporaryException ) {
|
||||
receiver.children[2].style.visibility = '';
|
||||
}
|
||||
receiver.children[1].textContent = '';
|
||||
receiver.children[1].appendChild(fragment);
|
||||
};
|
||||
|
||||
const fillSummaryPaneFilterList = async function(rows) {
|
||||
const rawFilter = targetRow.children[1].textContent;
|
||||
const compiledFilter = targetRow.getAttribute('data-filter');
|
||||
|
@ -1468,7 +1507,7 @@ const reloadTab = function(ev) {
|
|||
bestMatchFilter !== '' &&
|
||||
Array.isArray(response[bestMatchFilter])
|
||||
) {
|
||||
rows[0].children[1].textContent = bestMatchFilter;
|
||||
toSummaryPaneFilterNode(rows[0], bestMatchFilter);
|
||||
rows[1].children[1].appendChild(nodeFromFilter(
|
||||
bestMatchFilter,
|
||||
response[bestMatchFilter]
|
||||
|
@ -1499,7 +1538,7 @@ const reloadTab = function(ev) {
|
|||
});
|
||||
handleResponse(response);
|
||||
}
|
||||
};
|
||||
} ;
|
||||
|
||||
const fillSummaryPane = function() {
|
||||
const rows = dialog.querySelectorAll('.pane.details > div');
|
||||
|
@ -1508,12 +1547,12 @@ const reloadTab = function(ev) {
|
|||
const trch = tr.children;
|
||||
let text;
|
||||
// Filter and context
|
||||
text = trch[1].textContent;
|
||||
text = filterFromTargetRow();
|
||||
if (
|
||||
(text !== '') &&
|
||||
(trcl.contains('cosmeticRealm') || trcl.contains('networkRealm'))
|
||||
) {
|
||||
rows[0].children[1].textContent = text;
|
||||
toSummaryPaneFilterNode(rows[0], text);
|
||||
} else {
|
||||
rows[0].style.display = 'none';
|
||||
}
|
||||
|
@ -1753,7 +1792,7 @@ const reloadTab = function(ev) {
|
|||
fillSummaryPane();
|
||||
fillDynamicPane();
|
||||
fillStaticPane();
|
||||
dialog.addEventListener('click', onClick, true);
|
||||
dialog.addEventListener('click', ev => { onClick(ev); }, true);
|
||||
dialog.addEventListener('change', onSelectChange, true);
|
||||
dialog.addEventListener('input', onInputChange, true);
|
||||
modalDialog.show();
|
||||
|
|
|
@ -1203,10 +1203,11 @@ const extensionOriginURL = vAPI.getURL('');
|
|||
|
||||
const getLoggerData = async function(details, activeTabId, callback) {
|
||||
const response = {
|
||||
activeTabId,
|
||||
colorBlind: µb.userSettings.colorBlindFriendly,
|
||||
entries: µb.logger.readAll(details.ownerId),
|
||||
filterAuthorMode: µb.hiddenSettings.filterAuthorMode,
|
||||
maxEntries: µb.userSettings.requestLogMaxEntries,
|
||||
activeTabId: activeTabId,
|
||||
tabIdsToken: µb.pageStoresToken,
|
||||
tooltips: µb.userSettings.tooltipsDisabled === false
|
||||
};
|
||||
|
@ -1278,6 +1279,41 @@ const getURLFilteringData = function(details) {
|
|||
return response;
|
||||
};
|
||||
|
||||
const compileTemporaryException = function(filter) {
|
||||
const match = /#@?#/.exec(filter);
|
||||
if ( match === null ) { return; }
|
||||
let selector = filter.slice(match.index + match[0].length);
|
||||
let session;
|
||||
if ( selector.startsWith('+js') ) {
|
||||
session = µb.scriptletFilteringEngine.getSession();
|
||||
selector = selector.slice(4, -1).trim();
|
||||
} else {
|
||||
if ( selector.startsWith('^') ) {
|
||||
session = µb.htmlFilteringEngine.getSession();
|
||||
selector = selector.slice(1).trim();
|
||||
} else {
|
||||
session = µb.cosmeticFilteringEngine.getSession();
|
||||
}
|
||||
selector = µb.staticExtFilteringEngine.compileSelector(selector);
|
||||
}
|
||||
return { session, selector };
|
||||
};
|
||||
|
||||
const toggleTemporaryException = function(details) {
|
||||
const { session, selector } = compileTemporaryException(details.filter);
|
||||
if ( session.has(1, selector) ) {
|
||||
session.remove(1, selector);
|
||||
return false;
|
||||
}
|
||||
session.add(1, selector);
|
||||
return true;
|
||||
};
|
||||
|
||||
const hasTemporaryException = function(details) {
|
||||
const { session, selector } = compileTemporaryException(details.filter);
|
||||
return session && session.has(1, selector);
|
||||
};
|
||||
|
||||
const onMessage = function(request, sender, callback) {
|
||||
// Async
|
||||
switch ( request.what ) {
|
||||
|
@ -1301,6 +1337,10 @@ const onMessage = function(request, sender, callback) {
|
|||
let response;
|
||||
|
||||
switch ( request.what ) {
|
||||
case 'hasTemporaryException':
|
||||
response = hasTemporaryException(request);
|
||||
break;
|
||||
|
||||
case 'releaseView':
|
||||
if ( request.ownerId === µb.logger.ownerId ) {
|
||||
µb.logger.ownerId = undefined;
|
||||
|
@ -1327,6 +1367,10 @@ const onMessage = function(request, sender, callback) {
|
|||
response = getURLFilteringData(request);
|
||||
break;
|
||||
|
||||
case 'toggleTemporaryException':
|
||||
response = toggleTemporaryException(request);
|
||||
break;
|
||||
|
||||
default:
|
||||
return vAPI.messaging.UNHANDLED;
|
||||
}
|
||||
|
|
|
@ -342,6 +342,10 @@
|
|||
}
|
||||
};
|
||||
|
||||
api.getSession = function() {
|
||||
return scriptletDB.session;
|
||||
};
|
||||
|
||||
const scriptlets$ = new Set();
|
||||
const exceptions$ = new Set();
|
||||
const scriptletToCodeMap$ = new Map();
|
||||
|
@ -367,10 +371,8 @@
|
|||
scriptlets$.clear();
|
||||
exceptions$.clear();
|
||||
|
||||
scriptletDB.retrieve(
|
||||
hostname,
|
||||
[ scriptlets$, exceptions$ ]
|
||||
);
|
||||
scriptletDB.session.retrieve([ scriptlets$, exceptions$ ]);
|
||||
scriptletDB.retrieve(hostname, [ scriptlets$, exceptions$ ]);
|
||||
if ( request.entity !== '' ) {
|
||||
scriptletDB.retrieve(
|
||||
`${hostname.slice(0, -request.domain.length)}${request.entity}`,
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
|
||||
**/
|
||||
|
||||
µBlock.staticExtFilteringEngine = (function() {
|
||||
µBlock.staticExtFilteringEngine = (( ) => {
|
||||
const µb = µBlock;
|
||||
const reHasUnicode = /[^\x00-\x7F]/;
|
||||
const reParseRegexLiteral = /^\/(.+)\/([imu]+)?$/;
|
||||
|
@ -520,10 +520,45 @@
|
|||
// Avoid heterogeneous arrays. Thus:
|
||||
this.hostnameSlots = []; // array of integers
|
||||
// IMPORTANT: initialize with an empty array because -0 is NOT < 0.
|
||||
this.hostnameSlotsEx = [ [] ]; // Array of arrays of integers
|
||||
this.hostnameSlotsEx = [ [] ]; // array of arrays of integers
|
||||
// Array of strings (selectors and pseudo-selectors)
|
||||
this.strSlots = [];
|
||||
this.size = 0;
|
||||
// Temporary set
|
||||
this.session = {
|
||||
collection: new Map(),
|
||||
add: function(bits, s) {
|
||||
const bucket = this.collection.get(bits);
|
||||
if ( bucket === undefined ) {
|
||||
this.collection.set(bits, new Set([ s ]));
|
||||
} else {
|
||||
bucket.add(s);
|
||||
}
|
||||
},
|
||||
remove: function(bits, s) {
|
||||
const bucket = this.collection.get(bits);
|
||||
if ( bucket === undefined ) { return; }
|
||||
bucket.delete(s);
|
||||
if ( bucket.size !== 0 ) { return; }
|
||||
this.collection.delete(bits);
|
||||
},
|
||||
retrieve(out) {
|
||||
const mask = out.length - 1;
|
||||
for ( const [ bits, bucket ] of this.collection ) {
|
||||
for ( const s of bucket ) {
|
||||
out[bits & mask].add(s);
|
||||
}
|
||||
}
|
||||
},
|
||||
has(bits, s) {
|
||||
const selectors = this.collection.get(bits);
|
||||
return selectors !== undefined && selectors.has(s);
|
||||
},
|
||||
clear() {
|
||||
this.collection.clear();
|
||||
},
|
||||
};
|
||||
|
||||
if ( selfie !== undefined ) {
|
||||
this.fromSelfie(selfie);
|
||||
}
|
||||
|
@ -673,7 +708,7 @@
|
|||
// https://github.com/uBlockOrigin/uBlock-issues/issues/89
|
||||
// Do not discard unknown pseudo-elements.
|
||||
|
||||
api.compileSelector = (function() {
|
||||
api.compileSelector = (( ) => {
|
||||
const reAfterBeforeSelector = /^(.+?)(::?after|::?before|::[a-z-]+)$/;
|
||||
const reStyleSelector = /^(.+?):style\((.+?)\)$/;
|
||||
const reExtendedSyntax = /\[-(?:abp|ext)-[a-z-]+=(['"])(?:.+?)(?:\1)\]/;
|
||||
|
|
|
@ -112,7 +112,7 @@
|
|||
</div>
|
||||
<div class="panes">
|
||||
<div class="pane details" data-pane="details">
|
||||
<div><span data-i18n="loggerEntryDetailsFilter"></span><span></span></div>
|
||||
<div><span data-i18n="loggerEntryDetailsFilter"></span><span></span><span class="exceptor" style="visibility: collapse"></span></div>
|
||||
<div><span data-i18n="loggerEntryDetailsFilterList"></span><span class="prose"></span></div>
|
||||
<div><span data-i18n="loggerEntryDetailsRule"></span><span></span></div>
|
||||
<div><span data-i18n="loggerEntryDetailsRootContext"></span><span></span></div>
|
||||
|
|
Loading…
Reference in a new issue