[Feature]: Push event errors from page, components and query to debugger (#11428)

This commit is contained in:
vjaris42 2024-12-04 16:18:33 +05:30 committed by GitHub
parent 3ebc1933cc
commit 00f4ee4370
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 339 additions and 172 deletions

View file

@ -352,6 +352,7 @@ export const EventManager = ({
actionId: 'show-alert',
message: 'Hello world!',
alertType: 'info',
component: eventMetaDefinition.name,
...customEventRefs,
},
eventType: eventSourceType,

View file

@ -132,10 +132,7 @@ export const buttonConfig = {
borderRadius: {
type: 'numberInput',
displayName: 'Border radius',
validation: {
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] } },
defaultValue: false,
},
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: false },
accordian: 'button',
},
boxShadow: {

View file

@ -82,10 +82,7 @@ export const imageConfig = {
padding: {
type: 'code',
displayName: 'Padding',
validation: {
schema: { type: 'number' },
defaultValue: 0,
},
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 0 },
},
visibility: {
type: 'toggle',

View file

@ -20,10 +20,7 @@ export const numberinputConfig = {
value: {
type: 'code',
displayName: 'Default value',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 0,
},
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 0 },
},
placeholder: {
type: 'code',

View file

@ -149,6 +149,7 @@ export const createDebuggerSlice = (set, get) => ({
componentId: id,
},
logLevel: 'error',
errorTarget: 'Component Property',
timestamp: moment().toISOString(),
}));
@ -191,6 +192,7 @@ export const createDebuggerSlice = (set, get) => ({
effectiveProperty: { [property]: defaultValue },
componentId,
},
errorTarget: 'Component Property',
logLevel: 'error',
timestamp: moment().toISOString(),
});

View file

@ -15,6 +15,7 @@ import generateFile from '@/_lib/generate-file';
import urlJoin from 'url-join';
import { useCallback } from 'react';
import { navigate } from '@/AppBuilder/_utils/misc';
import moment from 'moment';
// To unsubscribe from the changes when no longer needed
// unsubscribe();
@ -211,32 +212,46 @@ export const createEventsSlice = (set, get) => ({
state.eventsSlice.module[moduleId].events = newEvents;
});
},
setTablePageIndex: (tableId, index = 1) => {
const { getExposedValueOfComponent } = get();
if (_.isEmpty(tableId)) {
console.log('No table is associated with this event.');
setTablePageIndex: (tableId, index, eventObj) => {
try {
const { getExposedValueOfComponent } = get();
if (typeof index !== 'number' && index !== undefined) {
throw new Error('Invalid page index.');
}
const exposedValue = getExposedValueOfComponent(tableId);
if (!exposedValue) {
throw new Error('No table is associated with this event.');
}
exposedValue.setPage(index);
return Promise.resolve();
} catch (error) {
get().eventsSlice.logError('set_table_page_index', 'set-table-page-index', error, eventObj, {
eventId: eventObj.eventType,
});
}
const exposedValue = getExposedValueOfComponent(tableId);
if (!exposedValue) {
console.log('No table is associated with this event.');
return Promise.resolve();
}
exposedValue.setPage(index);
return Promise.resolve();
},
showModal: (modal, show) => {
const { getExposedValueOfComponent } = get();
const modalId = modal?.id ?? modal;
console.log('modalId', modalId);
if (_.isEmpty(modalId)) {
console.log('No modal is associated with this event.');
return Promise.resolve();
}
const exposedValue = getExposedValueOfComponent(modalId);
show ? exposedValue.open() : exposedValue.close();
showModal: (modal, show, eventObj) => {
try {
const { getExposedValueOfComponent } = get();
const modalId = modal?.id ?? modal;
if (_.isEmpty(modalId)) {
throw new Error('No modal is associated with this event.');
}
const exposedValue = getExposedValueOfComponent(modalId);
show ? exposedValue.open() : exposedValue.close();
return Promise.resolve();
return Promise.resolve();
} catch (error) {
get().eventsSlice.logError(
show ? 'show_modal' : 'close_modal',
show ? 'show-modal' : 'close_modal',
error,
eventObj,
{
eventId: eventObj.eventType,
}
);
}
},
handleEvent: (eventName, events, options, moduleId = 'canvas') => {
const latestEvents = get().eventsSlice.getModuleEvents(moduleId);
@ -382,13 +397,84 @@ export const createEventsSlice = (set, get) => ({
?.sort((a, b) => a.index - b.index);
for (const event of filteredEvents) {
await get().eventsSlice.executeAction(event.event, mode, customVariables);
await get().eventsSlice.executeAction(event, mode, customVariables);
}
},
executeAction: debounce(async (event, mode, customVariables = {}) => {
logError(errorType, errorKind, error, eventObj = '', options = {}, logLevel) {
const { event = eventObj } = eventObj;
const pages = get().modules.canvas.pages;
const currentPageId = get().currentPageId;
const currentPage = pages.find((page) => page.id === currentPageId);
const componentIdMapping = get().modules['canvas'].componentNameIdMapping;
const componentName = Object.keys(componentIdMapping).find(
(key) => componentIdMapping[key] === eventObj?.sourceId
);
const componentId = eventObj?.sourceId;
const getSource = () => {
if (eventObj.eventType) {
return eventObj.eventType === 'data_query' ? 'query' : eventObj.eventType;
}
const sourceMap = {
onDataQueryFailure: 'query',
onDataQuerySuccess: 'query',
onPageLoad: 'page',
};
return sourceMap[event.eventId] || 'component';
};
const getQueryName = () => {
const queries = get().dataQuery.queries.modules.canvas;
return queries.find((query) => query.id === eventObj?.sourceId || '')?.name || '';
};
const constructErrorHeader = () => {
const source = getSource();
const pageName = currentPage.name;
const headerMap = {
component: `[Page ${pageName}] [Component ${componentName}] [Event ${event?.eventId}] [Action ${event.actionId}]`,
page: `[Page ${pageName}] [Event ${event.eventId}] [Action ${event.actionId}]`,
query: `[Query ${getQueryName()}] [Event ${event.eventId}] [Action ${event.actionId}]`,
};
return headerMap[source] || '';
};
const constructErrorTarget = () => {
const source = getSource();
const errorTargetMap = {
page: 'Event Errors with page',
component: 'Component Event',
query: 'Event Errors with query',
};
return errorTargetMap[source];
};
useStore.getState().debugger.log({
logLevel: logLevel ? logLevel : 'error',
type: errorType ? errorType : 'event',
kind: errorKind,
key: constructErrorHeader(),
error: {
message: error.message,
description: JSON.stringify(error.message, null, 2),
...(event.component && componentId && { componentId: componentId }),
},
errorTarget: constructErrorTarget(),
options: options,
strace: 'app_level',
timestamp: moment().toISOString(),
});
},
executeAction: debounce(async (eventObj, mode, customVariables = {}) => {
const { event = eventObj } = eventObj;
const { getExposedValueOfComponent, getResolvedValue } = get();
if (event.runOnlyIf) {
if (event?.runOnlyIf) {
const shouldRun = getResolvedValue(event.runOnlyIf, customVariables);
if (!shouldRun) {
return false;
@ -419,23 +505,37 @@ export const createEventsSlice = (set, get) => ({
return Promise.resolve();
}
case 'run-query': {
const { queryId, queryName } = event;
const params = event['parameters'];
const resolvedParams = {};
if (params) {
Object.keys(params).map((param) => (resolvedParams[param] = getResolvedValue(params[param], undefined)));
try {
const { queryId, queryName, component, eventId } = event;
const params = event['parameters'];
if (!queryId && !queryName) {
throw new Error('No query selected');
}
const resolvedParams = {};
if (params) {
Object.keys(params).map(
(param) => (resolvedParams[param] = getResolvedValue(params[param], undefined))
);
}
// !Todo tackle confirm query part once done
return get().queryPanel.runQuery(
queryId,
queryName,
undefined,
undefined,
resolvedParams,
component,
eventId,
false,
false,
'canvas'
);
} catch (error) {
get().eventsSlice.logError('run_query', 'run-query', error, eventObj, {
eventId: event.eventId,
});
return Promise.reject(error);
}
// !Todo tackle confirm query part once done
return get().queryPanel.runQuery(
queryId,
queryName,
undefined,
undefined,
resolvedParams,
false,
false,
'canvas'
);
}
case 'logout': {
return logoutAction();
@ -448,39 +548,47 @@ export const createEventsSlice = (set, get) => ({
return Promise.resolve();
}
case 'go-to-app': {
const resolvedValue = getResolvedValue(event.slug, customVariables);
const slug = resolvedValue;
const queryParams = event.queryParams?.reduce(
(result, queryParam) => ({
...result,
...{
[getResolvedValue(queryParam[0])]: getResolvedValue(queryParam[1], undefined, customVariables),
},
}),
{}
);
let url = `/applications/${slug}`;
if (queryParams) {
const queryPart = serializeNestedObjectToQueryParams(queryParams);
if (queryPart.length > 0) url = url + `?${queryPart}`;
}
if (mode === 'view') {
navigate(url);
} else {
if (confirm('The app will be opened in a new tab as the action is triggered from the editor.')) {
window.open(urlJoin(window.public_config?.TOOLJET_HOST, url));
try {
if (!event.slug) {
throw new Error('No application slug provided');
}
const resolvedValue = getResolvedValue(event.slug, customVariables);
const slug = resolvedValue;
const queryParams = event.queryParams?.reduce(
(result, queryParam) => ({
...result,
...{
[getResolvedValue(queryParam[0])]: getResolvedValue(queryParam[1], undefined, customVariables),
},
}),
{}
);
let url = `/applications/${slug}`;
if (queryParams) {
const queryPart = serializeNestedObjectToQueryParams(queryParams);
if (queryPart.length > 0) url = url + `?${queryPart}`;
}
if (mode === 'view') {
navigate(url);
} else {
if (confirm('The app will be opened in a new tab as the action is triggered from the editor.')) {
window.open(urlJoin(window.public_config?.TOOLJET_HOST, url));
}
}
return Promise.resolve();
} catch (error) {
get().eventsSlice.logError('go_to_app', 'go-to-app', error, eventObj, { eventId: event.eventId });
return Promise.reject();
}
return Promise.resolve();
}
case 'show-modal':
return get().eventsSlice.showModal(event.modal, true);
return get().eventsSlice.showModal(event.modal, true, eventObj);
case 'close-modal':
return get().eventsSlice.showModal(event.modal, false);
return get().eventsSlice.showModal(event.modal, false, eventObj);
case 'copy-to-clipboard': {
const contentToCopy = getResolvedValue(event.contentToCopy, customVariables);
copyToClipboard(contentToCopy);
@ -509,7 +617,7 @@ export const createEventsSlice = (set, get) => ({
}
case 'set-table-page': {
get().eventsSlice.setTablePageIndex(event.table, getResolvedValue(event.pageIndex));
get().eventsSlice.setTablePageIndex(event.table, getResolvedValue(event.pageIndex), eventObj);
break;
}
@ -632,85 +740,110 @@ export const createEventsSlice = (set, get) => ({
// return;
}
case 'control-component': {
// let component = Object.values(getCurrentState()?.components ?? {}).filter(
// (component) => component.id === event.componentId
// )[0];
const component = getExposedValueOfComponent(event.componentId);
const action = component?.[event.componentSpecificActionHandle];
// let action = '';
// let actionArguments = '';
// check if component id not found then try to find if its available as child widget else continue
// with normal flow finding action
// if (component == undefined) {
// component = _ref.appDefinition.pages[getCurrentState()?.page?.id].components[event.componentId].component;
// const parent = Object.values(getCurrentState()?.components ?? {}).find(
// (item) => item.id === component.parent
// );
// const child = Object.values(parent?.children).find((item) => item.id === event.componentId);
// if (child) {
// action = child[event.componentSpecificActionHandle];
// }
// } else {
// //normal component outside a container ex : form
// action = component?.[event.componentSpecificActionHandle];
// }
// actionArguments = _.map(event.componentSpecificActionParams, (param) => ({
// ...param,
// value: resolveReferences(param.value, undefined, customVariables),
// }));
// console.log('actionArguments', event.componentSpecificActionParams);
const actionArguments = event.componentSpecificActionParams.map((param) => {
const value = getResolvedValue(param.value, customVariables);
return {
...param,
value: value,
// value: resolveCode(re.valueWithBrackets, getAllExposedValues()),
};
});
// const actionArguments = _.map(event.componentSpecificActionParams, (param) => ({
// ...param,
// value: resolveReferences(param.value, getAllExposedValues(), customVariables),
// }));
try {
// let component = Object.values(getCurrentState()?.components ?? {}).filter(
// (component) => component.id === event.componentId
// )[0];
const { event } = eventObj;
if (!event.componentSpecificActionHandle) {
throw new Error('No component-specific action handle provided.');
}
const component = getExposedValueOfComponent(event.componentId);
if (!event.componentId || !Object.keys(component).length) {
throw new Error('No component ID provided for control-component action.');
}
const action = component?.[event.componentSpecificActionHandle];
// let action = '';
// let actionArguments = '';
// check if component id not found then try to find if its available as child widget else continue
// with normal flow finding action
// if (component == undefined) {
// component = _ref.appDefinition.pages[getCurrentState()?.page?.id].components[event.componentId].component;
// const parent = Object.values(getCurrentState()?.components ?? {}).find(
// (item) => item.id === component.parent
// );
// const child = Object.values(parent?.children).find((item) => item.id === event.componentId);
// if (child) {
// action = child[event.componentSpecificActionHandle];
// }
// } else {
// //normal component outside a container ex : form
// action = component?.[event.componentSpecificActionHandle];
// }
// actionArguments = _.map(event.componentSpecificActionParams, (param) => ({
// ...param,
// value: resolveReferences(param.value, undefined, customVariables),
// }));
// console.log('actionArguments', event.componentSpecificActionParams);
const actionArguments = event.componentSpecificActionParams.map((param) => {
const value = getResolvedValue(param.value, customVariables);
return {
...param,
value: value,
// value: resolveCode(re.valueWithBrackets, getAllExposedValues()),
};
});
// const actionArguments = _.map(event.componentSpecificActionParams, (param) => ({
// ...param,
// value: resolveReferences(param.value, getAllExposedValues(), customVariables),
// }));
const actionPromise = action && action(...actionArguments.map((argument) => argument.value));
return actionPromise ?? Promise.resolve();
const actionPromise = action && action(...actionArguments.map((argument) => argument.value));
return actionPromise ?? Promise.resolve();
} catch (error) {
get().eventsSlice.logError('control_component', 'control-component', error, eventObj, {
eventId: event.eventId,
});
return Promise.reject(error);
}
}
case 'switch-page': {
const { switchPage } = get();
const page = get().modules.canvas.pages.find((page) => page.id === event.pageId);
const queryParams = event.queryParams || [];
if (!page.disabled) {
const resolvedQueryParams = [];
queryParams.forEach((param) => {
resolvedQueryParams.push([
getResolvedValue(param[0], customVariables),
getResolvedValue(param[1], customVariables),
]);
});
const currentUrlParams = new URLSearchParams(window.location.search);
currentUrlParams.forEach((value, key) => {
if (key === 'version' || key === 'env') {
// if version or env is in current url query param but not in resolved params then add it to resolvedQueryParams
const exists = resolvedQueryParams.some(([resolvedKey]) => resolvedKey === key);
if (!exists) {
resolvedQueryParams.unshift([key, value]);
try {
const { pageId } = event;
if (!pageId) {
throw new Error('No page ID provided');
}
const { switchPage } = get();
const page = get().modules.canvas.pages.find((page) => page.id === event.pageId);
const queryParams = event.queryParams || [];
if (!page.disabled) {
const resolvedQueryParams = [];
queryParams.forEach((param) => {
resolvedQueryParams.push([
getResolvedValue(param[0], customVariables),
getResolvedValue(param[1], customVariables),
]);
});
const currentUrlParams = new URLSearchParams(window.location.search);
currentUrlParams.forEach((value, key) => {
if (key === 'version' || key === 'env') {
// if version or env is in current url query param but not in resolved params then add it to resolvedQueryParams
const exists = resolvedQueryParams.some(([resolvedKey]) => resolvedKey === key);
if (!exists) {
resolvedQueryParams.unshift([key, value]);
}
}
}
});
switchPage(page.id, page.handle, resolvedQueryParams);
} else {
toast.error('Page is disabled');
//!TODO push to debugger
get().debugger.log({
logLevel: 'error',
type: 'navToDisablePage',
kind: 'page',
message: `Attempt to switch to disabled page ${page.name} blocked.`,
error: 'Page is disabled',
});
switchPage(page.id, page.handle, resolvedQueryParams);
} else {
toast.error('Page is disabled');
//!TODO push to debugger
get().debugger.log({
logLevel: 'error',
type: 'navToDisablePage',
kind: 'page',
message: `Attempt to switch to disabled page ${page.name} blocked.`,
error: 'Page is disabled',
});
}
return Promise.resolve();
} catch (error) {
get().eventsSlice.logError('switch_page', 'switch-page', error, eventObj, {
eventId: event.eventId,
});
}
return Promise.resolve();
}
}
}

View file

@ -201,6 +201,8 @@ export const createQueryPanelSlice = (set, get) => ({
confirmed = undefined,
mode = 'edit',
userSuppliedParameters = {},
component,
eventId,
shouldSetPreviewData = false,
isOnLoad = false,
moduleId = 'canvas'
@ -247,8 +249,7 @@ export const createQueryPanelSlice = (set, get) => ({
if (query) {
dataQuery = JSON.parse(JSON.stringify(query));
} else {
toast.error('No query has been associated with the action.');
return;
throw new Error('No query selected');
}
if (_.isEmpty(parameters)) {
@ -298,6 +299,7 @@ export const createQueryPanelSlice = (set, get) => ({
isLoading: true,
data: [],
rawData: [],
id: queryId,
});
let queryExecutionPromise = null;
@ -367,6 +369,7 @@ export const createQueryPanelSlice = (set, get) => ({
kind: query.kind,
key: query.name,
message: errorData?.description,
errorTarget: 'Queries',
error:
query.kind === 'restapi'
? {
@ -435,6 +438,7 @@ export const createQueryPanelSlice = (set, get) => ({
key: query.name,
message: 'Query executed successfully',
isQuerySuccessLog: true,
errorTarget: 'Queries',
});
setResolvedQuery(queryId, {
@ -760,6 +764,7 @@ export const createQueryPanelSlice = (set, get) => ({
error: result,
isTransformation: true,
isQuerySuccessLog: result?.status === 'failed' ? false : true,
errorTarget: 'Queries',
});
return result;
},

View file

@ -8,12 +8,11 @@ import { useEditorActions, useEditorStore } from '@/_stores/editorStore';
function Logs({ logProps, idx }) {
const [open, setOpen] = React.useState(false);
let titleLogType = logProps?.type;
// need to change the titleLogType to query for transformations because if transformation fails, it is eventually a query failure
let titleLogType = logProps?.type !== 'event' ? logProps?.type : '';
if (titleLogType === 'transformations') {
titleLogType = 'query';
}
const title = ` [${capitalize(titleLogType)} ${logProps?.key}]`;
const title = logProps?.key;
const message =
logProps?.type === 'navToDisablePage'
? logProps?.message
@ -21,19 +20,21 @@ function Logs({ logProps, idx }) {
? 'Completed'
: logProps?.type === 'component'
? `Invalid property detected: ${logProps?.message}.`
: logProps?.type === 'Custom Log'
? logProps?.description
: `${startCase(logProps?.type)} failed: ${
logProps?.description ||
logProps?.message ||
(isString(logProps?.message) && logProps?.message) ||
(isString(logProps?.error?.description) && logProps?.error?.description) || //added string check since description can be an object. eg: runpy
logProps?.error?.message
logProps?.error?.message.trim()
}`;
const defaultStyles = {
transform: open ? 'rotate(90deg)' : 'rotate(0deg)',
transform: open ? 'rotate(0deg)' : 'rotate(-90deg)',
transition: '0.2s all',
display: logProps?.isQuerySuccessLog || logProps.type === 'navToDisablePage' ? 'none' : 'inline-block',
cursor: 'pointer',
paddingTop: '8px',
top: '8px',
pointerEvents: logProps?.isQuerySuccessLog || logProps.type === 'navToDisablePage' ? 'none' : 'default',
};
@ -85,20 +86,25 @@ function Logs({ logProps, idx }) {
onClick={(e) => {
setOpen((prev) => !prev);
}}
style={{ pointerEvents: logProps?.isQuerySuccessLog ? 'none' : 'default' }}
style={{ pointerEvents: logProps?.isQuerySuccessLog ? 'none' : 'default', position: 'relative' }}
>
<span className={cx('position-absolute')} style={defaultStyles}>
<SolidIcon name="cheveronright" width="16" />
<SolidIcon name="rightarrrow" fill={`var(--icons-strong)`} width="16" />
</span>
<span className="w-100" style={{ paddingTop: '8px', paddingBottom: '8px', paddingLeft: '20px' }}>
{logProps.type === 'navToDisablePage' ? (
renderNavToDisabledPageMessage()
) : (
<>
<span className="d-flex justify-content-between align-items-center text-truncate">
<span className="text-truncate text-slate-12">{title}</span>
<div className="d-flex align-items-center justify-content-between">
<div className="error-target cursor-pointer">{logProps?.errorTarget}</div>
<small className="text-slate-10 text-right ">{moment(logProps?.timestamp).fromNow()}</small>
</span>
</div>
<div className={`d-flex justify-content-between align-items-center ${!open && 'text-truncate'}`}>
<span className={` cursor-pointer debugger-error-title ${!open && 'text-truncate'}`}>
<HighlightSecondWord text={title} />
</span>
</div>
<span
className={cx('mx-1', {
'text-tomato-9': !logProps?.isQuerySuccessLog,
@ -134,3 +140,22 @@ function Logs({ logProps, idx }) {
let isString = (value) => typeof value === 'string' || value instanceof String;
export default Logs;
const HighlightSecondWord = ({ text }) => {
const processedText = text.split(/(\[.*?\])/).map((segment, index) => {
if (segment.startsWith('[') && segment.endsWith(']')) {
const content = segment.slice(1, -1).split(' ');
const firstWord = content[0];
const secondWord = content[1];
return (
<span key={index}>
[{firstWord} <b>{secondWord}</b>]
</span>
);
}
return segment;
});
return <span>{processedText}</span>;
};

View file

@ -232,11 +232,24 @@
}
.debugger-content {
padding: 0px 16px;
background-color: var(--base);
cursor: pointer;
hr {
margin-top: 0px !important;
margin-bottom: 16px !important;
margin: 0px !important;
margin-top: 16px !important;
}
&:hover {
background-color: var(--slate3);
}
.error-target {
background-color: var(--interactive-overlays-fill-hover) !important;
padding: 4px 7px;
border-radius: 7px;
color: var(--slate10)
}
}

View file

@ -14328,7 +14328,6 @@ color: var(--text-default);
.debugger-card-body {
margin-top: 8px;
margin-bottom: 16px;
padding: 0px 16px;
}
.left-sidebar-header-btn {
@ -14671,7 +14670,6 @@ color: var(--text-default);
.debugger-card-body {
margin-top: 8px;
margin-bottom: 16px;
padding: 0px 16px;
}
.left-sidebar-header-btn {
@ -15031,7 +15029,6 @@ color: var(--text-default);
.debugger-card-body {
margin-top: 8px;
margin-bottom: 16px;
padding: 0px 16px;
}
.left-sidebar-header-btn {