feat: State Inspector (#38368)
This commit is contained in:
parent
28d35ad903
commit
c2e4e11eb3
|
|
@ -137,8 +137,8 @@ const GitPullRequest = importRemixIcon(
|
|||
const GitRepository = importRemixIcon(
|
||||
async () => import("remixicon-react/GitRepositoryLineIcon"),
|
||||
);
|
||||
const GlobalLineIcon = importRemixIcon(
|
||||
async () => import("remixicon-react/GlobalLineIcon"),
|
||||
const GlobalLineIcon = importSvg(
|
||||
async () => import("../__assets__/icons/ads/globe-simple.svg"),
|
||||
);
|
||||
const GuideIcon = importRemixIcon(
|
||||
async () => import("remixicon-react/GuideFillIcon"),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 2.5C4.96243 2.5 2.5 4.96243 2.5 8C2.5 11.0376 4.96243 13.5 8 13.5C11.0376 13.5 13.5 11.0376 13.5 8C13.5 4.96243 11.0376 2.5 8 2.5ZM1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 8C1.5 7.72386 1.72386 7.5 2 7.5H14C14.2761 7.5 14.5 7.72386 14.5 8C14.5 8.27614 14.2761 8.5 14 8.5H2C1.72386 8.5 1.5 8.27614 1.5 8Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.69186 4.0692C6.27193 5.04973 6 6.43917 6 8.0001C6 9.56103 6.27193 10.9505 6.69186 11.931C6.9022 12.4222 7.13983 12.7877 7.37823 13.0231C7.61449 13.2564 7.82403 13.3376 8 13.3376C8.17597 13.3376 8.38551 13.2564 8.62177 13.0231C8.86017 12.7877 9.0978 12.4222 9.30814 11.931C9.72807 10.9505 10 9.56103 10 8.0001C10 6.43917 9.72807 5.04973 9.30814 4.0692C9.0978 3.57804 8.86017 3.21253 8.62177 2.97709C8.38551 2.74375 8.17597 2.6626 8 2.6626C7.82403 2.6626 7.61449 2.74375 7.37823 2.97709C7.13983 3.21253 6.9022 3.57804 6.69186 4.0692ZM6.67554 2.26559C7.03748 1.90814 7.48561 1.6626 8 1.6626C8.51439 1.6626 8.96253 1.90814 9.32446 2.26559C9.68425 2.62093 9.98533 3.1103 10.2274 3.67552C10.7123 4.80775 11 6.33707 11 8.0001C11 9.66313 10.7123 11.1924 10.2274 12.3247C9.98533 12.8899 9.68425 13.3793 9.32446 13.7346C8.96252 14.0921 8.51439 14.3376 8 14.3376C7.48561 14.3376 7.03748 14.0921 6.67554 13.7346C6.31575 13.3793 6.01467 12.8899 5.77261 12.3247C5.28771 11.1924 5 9.66313 5 8.0001C5 6.33707 5.28771 4.80775 5.77261 3.67552C6.01467 3.1103 6.31575 2.62093 6.67554 2.26559Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -28,6 +28,7 @@ const Container = styled.div<{ displayMode: ViewDisplayMode }>`
|
|||
|
||||
const ViewWrapper = styled.div`
|
||||
height: 100%;
|
||||
|
||||
&&& {
|
||||
ul.ads-v2-tabs__list {
|
||||
margin: 0 var(--ads-v2-spaces-8);
|
||||
|
|
@ -39,6 +40,7 @@ const ViewWrapper = styled.div`
|
|||
.ads-v2-tabs__list {
|
||||
padding: var(--ads-v2-spaces-1) var(--ads-v2-spaces-7);
|
||||
padding-left: var(--ads-v2-spaces-3);
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
} from "reducers/uiReducers/debuggerReducer";
|
||||
import type { EventName } from "ee/utils/analyticsUtilTypes";
|
||||
import type { APP_MODE } from "entities/App";
|
||||
import type { GenericEntityItem } from "ee/entities/IDE/constants";
|
||||
|
||||
export interface LogDebuggerErrorAnalyticsPayload {
|
||||
entityName: string;
|
||||
|
|
@ -147,3 +148,12 @@ export const showDebuggerLogs = () => {
|
|||
type: ReduxActionTypes.SHOW_DEBUGGER_LOGS,
|
||||
};
|
||||
};
|
||||
|
||||
export const setDebuggerStateInspectorSelectedItem = (
|
||||
payload: GenericEntityItem,
|
||||
) => {
|
||||
return {
|
||||
type: ReduxActionTypes.SET_DEBUGGER_STATE_INSPECTOR_SELECTED_ITEM,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import type { ReduxAction } from "ee/constants/ReduxActionConstants";
|
||||
import { ReduxActionTypes } from "ee/constants/ReduxActionConstants";
|
||||
import type { JSCollection, JSAction } from "entities/JSCollection";
|
||||
import {
|
||||
type ReduxAction,
|
||||
ReduxActionTypes,
|
||||
} from "ee/constants/ReduxActionConstants";
|
||||
import type { JSAction, JSCollection } from "entities/JSCollection";
|
||||
import type {
|
||||
RefactorAction,
|
||||
SetFunctionPropertyPayload,
|
||||
|
|
@ -10,6 +12,7 @@ import type {
|
|||
JSEditorTab,
|
||||
JSPaneDebuggerState,
|
||||
} from "reducers/uiReducers/jsPaneReducer";
|
||||
import type { JSUpdate } from "../utils/JSPaneUtils";
|
||||
|
||||
export const createNewJSCollection = (
|
||||
pageId: string,
|
||||
|
|
@ -132,3 +135,10 @@ export const setJsPaneDebuggerState = (
|
|||
type: ReduxActionTypes.SET_JS_PANE_DEBUGGER_STATE,
|
||||
payload,
|
||||
});
|
||||
|
||||
export const executeJSUpdates = (
|
||||
payload: Record<string, JSUpdate>,
|
||||
): ReduxAction<unknown> => ({
|
||||
type: ReduxActionTypes.EXECUTE_JS_UPDATES,
|
||||
payload,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
ReduxActionErrorTypes,
|
||||
ReduxActionTypes,
|
||||
} from "ee/constants/ReduxActionConstants";
|
||||
import type { JSUpdate } from "utils/JSPaneUtils";
|
||||
import type {
|
||||
Action,
|
||||
ActionViewMode,
|
||||
|
|
@ -343,13 +342,6 @@ export const executePageLoadActions = (
|
|||
};
|
||||
};
|
||||
|
||||
export const executeJSUpdates = (
|
||||
payload: Record<string, JSUpdate>,
|
||||
): ReduxAction<unknown> => ({
|
||||
type: ReduxActionTypes.EXECUTE_JS_UPDATES,
|
||||
payload,
|
||||
});
|
||||
|
||||
export const setActionsToExecuteOnPageLoad = (
|
||||
actions: Array<{
|
||||
executeOnLoad: boolean;
|
||||
|
|
@ -399,6 +391,7 @@ export interface updateActionDataPayloadType {
|
|||
actionDataPayload: actionDataPayload;
|
||||
parentSpan?: Span;
|
||||
}
|
||||
|
||||
export const updateActionData = (
|
||||
payload: actionDataPayload,
|
||||
parentSpan?: Span,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { usePluginActionContext } from "PluginActionEditor/PluginActionContext";
|
|||
import type { BottomTab } from "components/editorComponents/EntityBottomTabs";
|
||||
import { getIDEViewMode } from "selectors/ideSelectors";
|
||||
import { useSelector } from "react-redux";
|
||||
import { EditorViewMode } from "ee/entities/IDE/constants";
|
||||
import { EditorViewMode, IDE_TYPE } from "ee/entities/IDE/constants";
|
||||
import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/constants";
|
||||
import {
|
||||
createMessage,
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
DEBUGGER_HEADERS,
|
||||
DEBUGGER_LOGS,
|
||||
DEBUGGER_RESPONSE,
|
||||
DEBUGGER_STATE,
|
||||
} from "ee/constants/messages";
|
||||
import ErrorLogs from "components/editorComponents/Debugger/Errors";
|
||||
import DebuggerLogs from "components/editorComponents/Debugger/DebuggerLogs";
|
||||
|
|
@ -32,6 +33,9 @@ import {
|
|||
} from "PluginActionEditor/hooks";
|
||||
import useDebuggerTriggerClick from "components/editorComponents/Debugger/hooks/useDebuggerTriggerClick";
|
||||
import { Response } from "PluginActionEditor/components/PluginActionResponse/components/Response";
|
||||
import { StateInspector } from "components/editorComponents/Debugger/StateInspector";
|
||||
import { useLocation } from "react-router";
|
||||
import { getIDETypeByUrl } from "ee/entities/IDE/utils";
|
||||
|
||||
function usePluginActionResponseTabs() {
|
||||
const { action, actionResponse, datasource, plugin } =
|
||||
|
|
@ -108,7 +112,6 @@ function usePluginActionResponseTabs() {
|
|||
if (
|
||||
[
|
||||
PluginType.DB,
|
||||
PluginType.AI,
|
||||
PluginType.REMOTE,
|
||||
PluginType.SAAS,
|
||||
PluginType.INTERNAL,
|
||||
|
|
@ -145,6 +148,10 @@ function usePluginActionResponseTabs() {
|
|||
});
|
||||
}
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const ideType = getIDETypeByUrl(location.pathname);
|
||||
|
||||
if (IDEViewMode === EditorViewMode.FullScreen) {
|
||||
tabs.push(
|
||||
{
|
||||
|
|
@ -159,6 +166,14 @@ function usePluginActionResponseTabs() {
|
|||
panelComponent: <ErrorLogs />,
|
||||
},
|
||||
);
|
||||
|
||||
if (ideType === IDE_TYPE.App) {
|
||||
tabs.push({
|
||||
key: DEBUGGER_TAB_KEYS.STATE_TAB,
|
||||
title: createMessage(DEBUGGER_STATE),
|
||||
panelComponent: <StateInspector />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tabs;
|
||||
|
|
|
|||
|
|
@ -697,6 +697,8 @@ const IDEDebuggerActionTypes = {
|
|||
SET_JS_PANE_DEBUGGER_STATE: "SET_JS_PANE_DEBUGGER_STATE",
|
||||
SET_CANVAS_DEBUGGER_STATE: "SET_CANVAS_DEBUGGER_STATE",
|
||||
SHOW_DEBUGGER_LOGS: "SHOW_DEBUGGER_LOGS",
|
||||
SET_DEBUGGER_STATE_INSPECTOR_SELECTED_ITEM:
|
||||
"SET_DEBUGGER_STATE_INSPECTOR_SELECTED_ITEM",
|
||||
};
|
||||
|
||||
const ThemeActionTypes = {
|
||||
|
|
|
|||
|
|
@ -564,6 +564,7 @@ export const DEBUGGER_ERRORS = () => "Linter";
|
|||
export const DEBUGGER_RESPONSE = () => "Response";
|
||||
export const DEBUGGER_HEADERS = () => "Headers";
|
||||
export const DEBUGGER_LOGS = () => "Logs";
|
||||
export const DEBUGGER_STATE = () => "State";
|
||||
|
||||
export const INSPECT_ENTITY = () => "Inspect entity";
|
||||
export const INSPECT_ENTITY_BLANK_STATE = () => "Select an entity to inspect";
|
||||
|
|
|
|||
|
|
@ -129,6 +129,8 @@ export interface EntityItem {
|
|||
userPermissions?: string[];
|
||||
}
|
||||
|
||||
export interface GenericEntityItem extends Omit<EntityItem, "type"> {}
|
||||
|
||||
export type UseRoutes = Array<{
|
||||
key: string;
|
||||
// TODO: Fix this the next time the file is edited
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ export const JSListItem = (props: JSListItemProps) => {
|
|||
parentEntityType={parentEntityType}
|
||||
searchKeyword={""}
|
||||
step={1}
|
||||
type={item.type}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ import {
|
|||
import { countBy, find, get, groupBy, keyBy, sortBy } from "lodash";
|
||||
import ImageAlt from "assets/images/placeholder-image.svg";
|
||||
import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
|
||||
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
|
||||
import {
|
||||
MAIN_CONTAINER_WIDGET_ID,
|
||||
MAIN_CONTAINER_WIDGET_NAME,
|
||||
} from "constants/WidgetConstants";
|
||||
import type { AppStoreState } from "reducers/entityReducers/appReducer";
|
||||
import type {
|
||||
JSCollectionData,
|
||||
|
|
@ -59,10 +62,15 @@ import {
|
|||
import { MAX_DATASOURCE_SUGGESTIONS } from "constants/DatasourceEditorConstants";
|
||||
import type { CreateNewActionKeyInterface } from "ee/entities/Engine/actionHelpers";
|
||||
import { getNextEntityName } from "utils/AppsmithUtils";
|
||||
import { EditorEntityTab, type EntityItem } from "ee/entities/IDE/constants";
|
||||
import {
|
||||
EditorEntityTab,
|
||||
type EntityItem,
|
||||
type GenericEntityItem,
|
||||
} from "ee/entities/IDE/constants";
|
||||
import {
|
||||
ActionUrlIcon,
|
||||
JsFileIconV2,
|
||||
WidgetIconByType,
|
||||
} from "pages/Editor/Explorer/ExplorerIcons";
|
||||
import { getAssetUrl } from "ee/utils/airgapHelpers";
|
||||
import {
|
||||
|
|
@ -979,6 +987,18 @@ export const getAllPageWidgets = createSelector(
|
|||
},
|
||||
);
|
||||
|
||||
export const getUISegmentItems = createSelector(getCanvasWidgets, (widgets) => {
|
||||
const items: GenericEntityItem[] = Object.values(widgets)
|
||||
.filter((widget) => widget.widgetName !== MAIN_CONTAINER_WIDGET_NAME)
|
||||
.map((widget) => ({
|
||||
icon: WidgetIconByType(widget.type),
|
||||
title: widget.widgetName,
|
||||
key: widget.widgetId,
|
||||
}));
|
||||
|
||||
return items;
|
||||
});
|
||||
|
||||
export const getPageList = createSelector(
|
||||
(state: AppState) => state.entities.pageList.pages,
|
||||
(pages) => pages,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback } from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import DebuggerLogs from "./DebuggerLogs";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
|
|
@ -17,11 +17,16 @@ import {
|
|||
createMessage,
|
||||
DEBUGGER_ERRORS,
|
||||
DEBUGGER_LOGS,
|
||||
DEBUGGER_STATE,
|
||||
} from "ee/constants/messages";
|
||||
import { DEBUGGER_TAB_KEYS } from "./constants";
|
||||
import EntityBottomTabs from "../EntityBottomTabs";
|
||||
import { ActionExecutionResizerHeight } from "PluginActionEditor/components/PluginActionResponse/constants";
|
||||
import { IDEBottomView, ViewHideBehaviour, ViewDisplayMode } from "IDE";
|
||||
import { StateInspector } from "./StateInspector";
|
||||
import { getIDETypeByUrl } from "ee/entities/IDE/utils";
|
||||
import { useLocation } from "react-router";
|
||||
import { IDE_TYPE } from "ee/entities/IDE/constants";
|
||||
|
||||
function DebuggerTabs() {
|
||||
const dispatch = useDispatch();
|
||||
|
|
@ -31,33 +36,59 @@ function DebuggerTabs() {
|
|||
// get the height of the response pane.
|
||||
const responsePaneHeight = useSelector(getResponsePaneHeight);
|
||||
// set the height of the response pane.
|
||||
const updateResponsePaneHeight = useCallback((height: number) => {
|
||||
dispatch(setResponsePaneHeight(height));
|
||||
}, []);
|
||||
const setSelectedTab = (tabKey: string) => {
|
||||
if (tabKey === DEBUGGER_TAB_KEYS.ERROR_TAB) {
|
||||
AnalyticsUtil.logEvent("OPEN_DEBUGGER", {
|
||||
source: "WIDGET_EDITOR",
|
||||
const updateResponsePaneHeight = useCallback(
|
||||
(height: number) => {
|
||||
dispatch(setResponsePaneHeight(height));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const setSelectedTab = useCallback(
|
||||
(tabKey: string) => {
|
||||
if (tabKey === DEBUGGER_TAB_KEYS.ERROR_TAB) {
|
||||
AnalyticsUtil.logEvent("OPEN_DEBUGGER", {
|
||||
source: "WIDGET_EDITOR",
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(setDebuggerSelectedTab(tabKey));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
dispatch(showDebugger(false));
|
||||
}, [dispatch]);
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const ideType = getIDETypeByUrl(location.pathname);
|
||||
|
||||
const DEBUGGER_TABS = useMemo(() => {
|
||||
const tabs = [
|
||||
{
|
||||
key: DEBUGGER_TAB_KEYS.LOGS_TAB,
|
||||
title: createMessage(DEBUGGER_LOGS),
|
||||
panelComponent: <DebuggerLogs hasShortCut />,
|
||||
},
|
||||
{
|
||||
key: DEBUGGER_TAB_KEYS.ERROR_TAB,
|
||||
title: createMessage(DEBUGGER_ERRORS),
|
||||
count: errorCount,
|
||||
panelComponent: <Errors hasShortCut />,
|
||||
},
|
||||
];
|
||||
|
||||
if (ideType === IDE_TYPE.App) {
|
||||
tabs.push({
|
||||
key: DEBUGGER_TAB_KEYS.STATE_TAB,
|
||||
title: createMessage(DEBUGGER_STATE),
|
||||
panelComponent: <StateInspector />,
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(setDebuggerSelectedTab(tabKey));
|
||||
};
|
||||
const onClose = () => dispatch(showDebugger(false));
|
||||
|
||||
const DEBUGGER_TABS = [
|
||||
{
|
||||
key: DEBUGGER_TAB_KEYS.LOGS_TAB,
|
||||
title: createMessage(DEBUGGER_LOGS),
|
||||
panelComponent: <DebuggerLogs hasShortCut />,
|
||||
},
|
||||
{
|
||||
key: DEBUGGER_TAB_KEYS.ERROR_TAB,
|
||||
title: createMessage(DEBUGGER_ERRORS),
|
||||
count: errorCount,
|
||||
panelComponent: <Errors hasShortCut />,
|
||||
},
|
||||
];
|
||||
return tabs;
|
||||
}, [errorCount, ideType]);
|
||||
|
||||
// Do not render if response, header or schema tab is selected in the bottom bar.
|
||||
const shouldRender = !(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
import React from "react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { StateInspector } from "./StateInspector";
|
||||
import { useStateInspectorItems } from "./hooks";
|
||||
import { filterEntityGroupsBySearchTerm } from "IDE/utils";
|
||||
|
||||
jest.mock("./hooks");
|
||||
jest.mock("IDE/utils");
|
||||
|
||||
const mockedUseStateInspectorItems = useStateInspectorItems as jest.Mock;
|
||||
const mockedFilterEntityGroupsBySearchTerm =
|
||||
filterEntityGroupsBySearchTerm as jest.Mock;
|
||||
|
||||
describe("StateInspector", () => {
|
||||
beforeEach(() => {
|
||||
mockedFilterEntityGroupsBySearchTerm.mockImplementation(
|
||||
(searchTerm, items) =>
|
||||
items.filter((item: { group: string }) =>
|
||||
item.group.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("renders search input and filters items based on search term", () => {
|
||||
mockedUseStateInspectorItems.mockReturnValue([
|
||||
{ title: "Item 1", icon: "icon1", code: { key: "value1" } },
|
||||
[
|
||||
{ group: "Group 1", items: [{ title: "Item 1" }] },
|
||||
{ group: "Group 2", items: [{ title: "Item 2" }] },
|
||||
],
|
||||
{ key: "value1" },
|
||||
]);
|
||||
render(<StateInspector />);
|
||||
const searchInput = screen.getByPlaceholderText("Search entities");
|
||||
|
||||
fireEvent.change(searchInput, { target: { value: "Group 1" } });
|
||||
expect(screen.getByText("Group 1")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Group 2")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Calls the onClick of the item", () => {
|
||||
const mockOnClick = jest.fn();
|
||||
|
||||
mockedUseStateInspectorItems.mockReturnValue([
|
||||
{ title: "Item 1", icon: "icon1", code: { key: "value1" } },
|
||||
[
|
||||
{ group: "Group 1", items: [{ title: "Item 1" }] },
|
||||
{
|
||||
group: "Group 2",
|
||||
items: [{ title: "Item 2", onClick: mockOnClick }],
|
||||
},
|
||||
],
|
||||
{ key: "value1" },
|
||||
]);
|
||||
render(<StateInspector />);
|
||||
fireEvent.click(screen.getByText("Item 2"));
|
||||
|
||||
expect(mockOnClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Renders the selected item details", () => {
|
||||
mockedUseStateInspectorItems.mockReturnValue([
|
||||
{ title: "Item 1", icon: "icon1", code: { key: "value1" } },
|
||||
[
|
||||
{ group: "Group 1", items: [{ title: "Item 1" }] },
|
||||
{
|
||||
group: "Group 2",
|
||||
items: [{ title: "Item 2" }],
|
||||
},
|
||||
],
|
||||
{ key: "Value1" },
|
||||
]);
|
||||
render(<StateInspector />);
|
||||
|
||||
expect(
|
||||
screen.getByTestId("t--selected-entity-details").textContent,
|
||||
).toContain("Item 1");
|
||||
|
||||
expect(
|
||||
screen.getByTestId("t--selected-entity-details").textContent,
|
||||
).toContain("Value1");
|
||||
});
|
||||
|
||||
it("does not render selected item details when no item is selected", () => {
|
||||
mockedUseStateInspectorItems.mockReturnValue([null, [], null]);
|
||||
render(<StateInspector />);
|
||||
expect(screen.queryByText("Item 1")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders all items when search term is empty", () => {
|
||||
mockedUseStateInspectorItems.mockReturnValue([
|
||||
{ title: "Item 1", icon: "icon1", code: { key: "value1" } },
|
||||
[
|
||||
{ group: "Group 1", items: [{ title: "Item 1" }] },
|
||||
{
|
||||
group: "Group 2",
|
||||
items: [{ title: "Item 2" }],
|
||||
},
|
||||
],
|
||||
{ key: "value1" },
|
||||
]);
|
||||
|
||||
render(<StateInspector />);
|
||||
expect(screen.getByText("Group 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Group 2")).toBeInTheDocument();
|
||||
});
|
||||
it("renders no items when search term does not match any group", () => {
|
||||
render(<StateInspector />);
|
||||
const searchInput = screen.getByPlaceholderText("Search entities");
|
||||
|
||||
fireEvent.change(searchInput, { target: { value: "Nonexistent Group" } });
|
||||
expect(screen.queryByText("Group 1")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Group 2")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders no items when items list is empty", () => {
|
||||
mockedUseStateInspectorItems.mockReturnValue([null, [], null]);
|
||||
render(<StateInspector />);
|
||||
expect(screen.queryByText("Group 1")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Group 2")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders correctly when selected item has no code", () => {
|
||||
mockedUseStateInspectorItems.mockReturnValue([
|
||||
{ title: "Item 1", icon: "icon1", code: null },
|
||||
[
|
||||
{ group: "Group 1", items: [{ title: "Item 1" }] },
|
||||
{ group: "Group 2", items: [{ title: "Item 2" }] },
|
||||
],
|
||||
{},
|
||||
]);
|
||||
render(<StateInspector />);
|
||||
|
||||
expect(
|
||||
screen.getByTestId("t--selected-entity-details").textContent,
|
||||
).toContain("Item 1");
|
||||
|
||||
expect(
|
||||
screen.getByTestId("t--selected-entity-details").textContent,
|
||||
).toContain("0 items");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import React, { useState } from "react";
|
||||
import ReactJson from "react-json-view";
|
||||
import {
|
||||
Flex,
|
||||
List,
|
||||
type ListItemProps,
|
||||
SearchInput,
|
||||
Text,
|
||||
} from "@appsmith/ads";
|
||||
import { filterEntityGroupsBySearchTerm } from "IDE/utils";
|
||||
import { useStateInspectorItems } from "./hooks";
|
||||
import * as Styled from "./styles";
|
||||
|
||||
export const reactJsonProps = {
|
||||
name: null,
|
||||
enableClipboard: false,
|
||||
displayDataTypes: false,
|
||||
displayArrayKey: true,
|
||||
quotesOnKeys: false,
|
||||
style: {
|
||||
fontSize: "12px",
|
||||
},
|
||||
collapsed: 1,
|
||||
indentWidth: 2,
|
||||
collapseStringsAfterLength: 30,
|
||||
};
|
||||
|
||||
export const StateInspector = () => {
|
||||
const [selectedItem, items, selectedItemCode] = useStateInspectorItems();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
const filteredItemGroups = filterEntityGroupsBySearchTerm<
|
||||
{ group: string },
|
||||
ListItemProps
|
||||
>(searchTerm, items);
|
||||
|
||||
return (
|
||||
<Flex h="calc(100% - 40px)" overflow="hidden" w="100%">
|
||||
<Flex
|
||||
borderRight="1px solid var(--ads-v2-color-border)"
|
||||
flexDirection="column"
|
||||
h="100%"
|
||||
overflowY="hidden"
|
||||
w="400px"
|
||||
>
|
||||
<Flex p="spaces-3">
|
||||
<SearchInput
|
||||
onChange={setSearchTerm}
|
||||
placeholder="Search entities"
|
||||
value={searchTerm}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
gap="spaces-3"
|
||||
overflowY="auto"
|
||||
pl="spaces-3"
|
||||
pr="spaces-3"
|
||||
>
|
||||
{filteredItemGroups.map((item) => (
|
||||
<Styled.Group
|
||||
flexDirection="column"
|
||||
gap="spaces-2"
|
||||
key={item.group}
|
||||
>
|
||||
<Styled.GroupName
|
||||
className="overflow-hidden overflow-ellipsis whitespace-nowrap flex-shrink-0"
|
||||
kind="body-s"
|
||||
>
|
||||
{item.group}
|
||||
</Styled.GroupName>
|
||||
<List items={item.items} />
|
||||
</Styled.Group>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
{selectedItem ? (
|
||||
<Flex
|
||||
className="mp-mask"
|
||||
data-testid="t--selected-entity-details"
|
||||
flex="1"
|
||||
flexDirection="column"
|
||||
overflowY="hidden"
|
||||
>
|
||||
<Styled.SelectedItem
|
||||
alignItems="center"
|
||||
flexDirection="row"
|
||||
gap="spaces-2"
|
||||
p="spaces-3"
|
||||
>
|
||||
{selectedItem.icon}
|
||||
<Text kind="body-m">{selectedItem.title}</Text>
|
||||
</Styled.SelectedItem>
|
||||
<Flex overflowY="auto" px="spaces-3">
|
||||
<ReactJson src={selectedItemCode} {...reactJsonProps} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
) : null}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { useStateInspectorItems } from "./useStateInspectorItems";
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { GlobeIcon } from "pages/Editor/Explorer/ExplorerIcons";
|
||||
import type { GetGroupHookType } from "../types";
|
||||
|
||||
export const useGetGlobalItemsForStateInspector: GetGroupHookType = () => {
|
||||
const appsmithObj = {
|
||||
key: "appsmith",
|
||||
title: "appsmith",
|
||||
icon: GlobeIcon(),
|
||||
};
|
||||
|
||||
const appsmithItems = [
|
||||
{
|
||||
id: appsmithObj.key,
|
||||
title: appsmithObj.title,
|
||||
startIcon: appsmithObj.icon,
|
||||
},
|
||||
];
|
||||
|
||||
return { group: "Globals", items: appsmithItems };
|
||||
};
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { useSelector } from "react-redux";
|
||||
import { getJSSegmentItems } from "ee/selectors/entitiesSelector";
|
||||
import type { GetGroupHookType } from "../types";
|
||||
|
||||
export const useGetJSItemsForStateInspector: GetGroupHookType = () => {
|
||||
const jsObjects = useSelector(getJSSegmentItems);
|
||||
|
||||
const jsItems = jsObjects.map((jsObject) => ({
|
||||
id: jsObject.key,
|
||||
title: jsObject.title,
|
||||
startIcon: jsObject.icon,
|
||||
}));
|
||||
|
||||
return { group: "JS objects", items: jsItems };
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { useSelector } from "react-redux";
|
||||
import { getQuerySegmentItems } from "ee/selectors/entitiesSelector";
|
||||
import type { GetGroupHookType } from "../types";
|
||||
|
||||
export const useGetQueryItemsForStateInspector: GetGroupHookType = () => {
|
||||
const queries = useSelector(getQuerySegmentItems);
|
||||
|
||||
const queryItems = queries.map((query) => ({
|
||||
id: query.key,
|
||||
title: query.title,
|
||||
startIcon: query.icon,
|
||||
className: "query-item",
|
||||
}));
|
||||
|
||||
return { group: "Queries", items: queryItems };
|
||||
};
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { useSelector } from "react-redux";
|
||||
import { getUISegmentItems } from "ee/selectors/entitiesSelector";
|
||||
import type { GetGroupHookType } from "../types";
|
||||
|
||||
export const useGetUIItemsForStateInspector: GetGroupHookType = () => {
|
||||
const widgets = useSelector(getUISegmentItems);
|
||||
|
||||
const widgetItems = widgets.map((widget) => ({
|
||||
id: widget.key,
|
||||
title: widget.title,
|
||||
startIcon: widget.icon,
|
||||
}));
|
||||
|
||||
return { group: "UI elements", items: widgetItems };
|
||||
};
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import { useEffect, useMemo } from "react";
|
||||
import { useStateInspectorState } from "./useStateInspectorState";
|
||||
import { useGetGlobalItemsForStateInspector } from "./useGetGlobalItemsForStateInspector";
|
||||
import { useGetQueryItemsForStateInspector } from "./useGetQueryItemsForStateInspector";
|
||||
import { useGetJSItemsForStateInspector } from "./useGetJSItemsForStateInspector";
|
||||
import { useGetUIItemsForStateInspector } from "./useGetUIItemsForStateInspector";
|
||||
import type { GroupedItems } from "../types";
|
||||
import { enhanceItemForListItem } from "../utils";
|
||||
import type { GenericEntityItem } from "ee/entities/IDE/constants";
|
||||
import { filterInternalProperties } from "utils/FilterInternalProperties";
|
||||
import { getConfigTree, getDataTree } from "selectors/dataTreeSelectors";
|
||||
import { getJSCollections } from "ee/selectors/entitiesSelector";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
export const useStateInspectorItems: () => [
|
||||
GenericEntityItem | undefined,
|
||||
GroupedItems[],
|
||||
unknown,
|
||||
] = () => {
|
||||
const [selectedItem, setSelectedItem] = useStateInspectorState();
|
||||
|
||||
const queries = useGetQueryItemsForStateInspector();
|
||||
const jsItems = useGetJSItemsForStateInspector();
|
||||
const uiItems = useGetUIItemsForStateInspector();
|
||||
const globalItems = useGetGlobalItemsForStateInspector();
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const returnValue: GroupedItems[] = [];
|
||||
|
||||
if (queries.items.length) {
|
||||
returnValue.push({
|
||||
...queries,
|
||||
items: queries.items.map((query) =>
|
||||
enhanceItemForListItem(query, selectedItem, setSelectedItem),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (jsItems.items.length) {
|
||||
returnValue.push({
|
||||
...jsItems,
|
||||
items: jsItems.items.map((jsItem) =>
|
||||
enhanceItemForListItem(jsItem, selectedItem, setSelectedItem),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (uiItems.items.length) {
|
||||
returnValue.push({
|
||||
...uiItems,
|
||||
items: uiItems.items.map((uiItem) =>
|
||||
enhanceItemForListItem(uiItem, selectedItem, setSelectedItem),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (globalItems.items.length) {
|
||||
returnValue.push({
|
||||
...globalItems,
|
||||
items: globalItems.items.map((globalItem) =>
|
||||
enhanceItemForListItem(globalItem, selectedItem, setSelectedItem),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}, [globalItems, jsItems, queries, selectedItem, setSelectedItem, uiItems]);
|
||||
|
||||
const dataTree = useSelector(getDataTree);
|
||||
const configTree = useSelector(getConfigTree);
|
||||
const jsActions = useSelector(getJSCollections);
|
||||
let filteredData: unknown = "";
|
||||
|
||||
if (selectedItem && selectedItem.title in dataTree) {
|
||||
filteredData = filterInternalProperties(
|
||||
selectedItem.title,
|
||||
dataTree[selectedItem.title],
|
||||
jsActions,
|
||||
dataTree,
|
||||
configTree,
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(
|
||||
function handleNoItemSelected() {
|
||||
if (!selectedItem || !(selectedItem.title in dataTree)) {
|
||||
const firstItem = groups[0].items[0];
|
||||
|
||||
setSelectedItem({
|
||||
key: firstItem.id as string,
|
||||
icon: firstItem.startIcon,
|
||||
title: firstItem.title,
|
||||
});
|
||||
}
|
||||
},
|
||||
[dataTree, groups, selectedItem, setSelectedItem],
|
||||
);
|
||||
|
||||
return [selectedItem, groups, filteredData];
|
||||
};
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { useDispatch, useSelector } from "react-redux";
|
||||
import type { GenericEntityItem } from "ee/entities/IDE/constants";
|
||||
import { setDebuggerStateInspectorSelectedItem } from "actions/debuggerActions";
|
||||
import { getDebuggerStateInspectorSelectedItem } from "selectors/debuggerSelectors";
|
||||
|
||||
export const useStateInspectorState: () => [
|
||||
GenericEntityItem | undefined,
|
||||
(item: GenericEntityItem) => void,
|
||||
] = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const setSelectedItem = (item: GenericEntityItem) => {
|
||||
dispatch(setDebuggerStateInspectorSelectedItem(item));
|
||||
};
|
||||
|
||||
const selectedItem = useSelector(getDebuggerStateInspectorSelectedItem);
|
||||
|
||||
return [selectedItem, setSelectedItem];
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { StateInspector } from "./StateInspector";
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import styled, { css } from "styled-components";
|
||||
import { Flex, Text } from "@appsmith/ads";
|
||||
|
||||
const imgSizer = css`
|
||||
img {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Group = styled(Flex)`
|
||||
.query-item {
|
||||
${imgSizer}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GroupName = styled(Text)`
|
||||
padding: var(--ads-v2-spaces-1) var(--ads-v2-spaces-3);
|
||||
`;
|
||||
|
||||
export const SelectedItem = styled(Flex)`
|
||||
${imgSizer}
|
||||
`;
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import type { ListItemProps } from "@appsmith/ads";
|
||||
|
||||
export interface GroupedItems {
|
||||
group: string;
|
||||
items: ListItemProps[];
|
||||
}
|
||||
|
||||
export interface ListItemWithoutOnClick extends Omit<ListItemProps, "onClick"> {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type GetGroupHookType = () => {
|
||||
group: string;
|
||||
items: ListItemWithoutOnClick[];
|
||||
};
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import type { ListItemWithoutOnClick } from "./types";
|
||||
import type { ListItemProps } from "@appsmith/ads";
|
||||
import type { GenericEntityItem } from "ee/entities/IDE/constants";
|
||||
|
||||
export const enhanceItemForListItem = (
|
||||
item: ListItemWithoutOnClick,
|
||||
selectedItem: GenericEntityItem | undefined,
|
||||
setSelectedItem: (item: GenericEntityItem) => void,
|
||||
): ListItemProps => {
|
||||
return {
|
||||
...item,
|
||||
isSelected: selectedItem ? selectedItem.key === item.id : false,
|
||||
onClick: () =>
|
||||
setSelectedItem({
|
||||
key: item.id,
|
||||
title: item.title,
|
||||
icon: item.startIcon,
|
||||
}),
|
||||
size: "md",
|
||||
};
|
||||
};
|
||||
|
|
@ -5,4 +5,5 @@ export enum DEBUGGER_TAB_KEYS {
|
|||
HEADER_TAB = "HEADERS_TAB",
|
||||
ERROR_TAB = "ERROR_TAB",
|
||||
LOGS_TAB = "LOGS_TAB",
|
||||
STATE_TAB = "STATE_TAB",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { connect, useDispatch, useSelector } from "react-redux";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import { withRouter } from "react-router";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import type { JSEditorRouteParams } from "constants/routes";
|
||||
import {
|
||||
createMessage,
|
||||
DEBUGGER_ERRORS,
|
||||
DEBUGGER_LOGS,
|
||||
DEBUGGER_RESPONSE,
|
||||
DEBUGGER_STATE,
|
||||
EXECUTING_FUNCTION,
|
||||
NO_JS_FUNCTION_RETURN_VALUE,
|
||||
UPDATING_JS_COLLECTION,
|
||||
|
|
@ -35,11 +32,15 @@ import {
|
|||
import { getJsPaneDebuggerState } from "selectors/jsPaneSelectors";
|
||||
import { setJsPaneDebuggerState } from "actions/jsPaneActions";
|
||||
import { getIDEViewMode } from "selectors/ideSelectors";
|
||||
import { EditorViewMode } from "ee/entities/IDE/constants";
|
||||
import { EditorViewMode, IDE_TYPE } from "ee/entities/IDE/constants";
|
||||
import ErrorLogs from "./Debugger/Errors";
|
||||
import { isBrowserExecutionAllowed } from "ee/utils/actionExecutionUtils";
|
||||
import JSRemoteExecutionView from "ee/components/JSRemoteExecutionView";
|
||||
import { IDEBottomView, ViewHideBehaviour } from "IDE";
|
||||
import { StateInspector } from "./Debugger/StateInspector";
|
||||
import { getErrorCount } from "selectors/debuggerSelectors";
|
||||
import { getIDETypeByUrl } from "ee/entities/IDE/utils";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
const ResponseTabWrapper = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -61,43 +62,54 @@ const NoReturnValueWrapper = styled.div`
|
|||
padding-top: ${(props) => props.theme.spaces[6]}px;
|
||||
`;
|
||||
|
||||
interface ReduxStateProps {
|
||||
errorCount: number;
|
||||
interface Props {
|
||||
currentFunction: JSAction | null;
|
||||
theme?: EditorTheme;
|
||||
errors: Array<EvaluationError>;
|
||||
disabled: boolean;
|
||||
isLoading: boolean;
|
||||
onButtonClick: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
jsCollectionData: JSCollectionData | undefined;
|
||||
debuggerLogsDefaultName?: string;
|
||||
}
|
||||
|
||||
type Props = ReduxStateProps &
|
||||
RouteComponentProps<JSEditorRouteParams> & {
|
||||
currentFunction: JSAction | null;
|
||||
theme?: EditorTheme;
|
||||
errors: Array<EvaluationError>;
|
||||
disabled: boolean;
|
||||
isLoading: boolean;
|
||||
onButtonClick: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
jsCollectionData: JSCollectionData | undefined;
|
||||
debuggerLogsDefaultName?: string;
|
||||
};
|
||||
|
||||
function JSResponseView(props: Props) {
|
||||
const {
|
||||
currentFunction,
|
||||
disabled,
|
||||
errorCount,
|
||||
errors,
|
||||
isLoading,
|
||||
jsCollectionData,
|
||||
onButtonClick,
|
||||
theme,
|
||||
} = props;
|
||||
const [responseStatus, setResponseStatus] = useState<JSResponseState>(
|
||||
JSResponseState.NoResponse,
|
||||
);
|
||||
const responses = (jsCollectionData && jsCollectionData.data) || {};
|
||||
const isDirty = (jsCollectionData && jsCollectionData.isDirty) || {};
|
||||
const isExecuting = (jsCollectionData && jsCollectionData.isExecuting) || {};
|
||||
const errorCount = useSelector(getErrorCount);
|
||||
|
||||
const { isDirty, isExecuting, responses } = useMemo(() => {
|
||||
return {
|
||||
responses: (jsCollectionData && jsCollectionData.data) || {},
|
||||
isDirty: (jsCollectionData && jsCollectionData.isDirty) || {},
|
||||
isExecuting: (jsCollectionData && jsCollectionData.isExecuting) || {},
|
||||
};
|
||||
}, [jsCollectionData]);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const response =
|
||||
currentFunction && currentFunction.id && currentFunction.id in responses
|
||||
? responses[currentFunction.id]
|
||||
: "";
|
||||
|
||||
const response = useMemo(() => {
|
||||
if (
|
||||
!currentFunction ||
|
||||
!currentFunction.id ||
|
||||
!(currentFunction.id in responses)
|
||||
) {
|
||||
return { value: "" };
|
||||
}
|
||||
|
||||
return { value: responses[currentFunction.id] as string };
|
||||
}, [currentFunction, responses]);
|
||||
|
||||
// parse error found while trying to execute function
|
||||
const hasExecutionParseErrors = responseStatus === JSResponseState.IsDirty;
|
||||
// error found while trying to parse JS Object
|
||||
|
|
@ -122,94 +134,119 @@ function JSResponseView(props: Props) {
|
|||
);
|
||||
}, [jsCollectionData?.config, currentFunction]);
|
||||
|
||||
const JSResponseTab = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
{localExecutionAllowed && hasExecutionParseErrors && (
|
||||
<ResponseErrorContainer>
|
||||
<ResponseErrorContent>
|
||||
<div className="t--js-response-parse-error-call-out">
|
||||
Function failed to execute. Check logs for more information.
|
||||
</div>
|
||||
</ResponseErrorContent>
|
||||
</ResponseErrorContainer>
|
||||
)}
|
||||
<ResponseTabWrapper
|
||||
className={errors.length && localExecutionAllowed ? "disable" : ""}
|
||||
>
|
||||
<Flex px="spaces-7" width="100%">
|
||||
<>
|
||||
{localExecutionAllowed && (
|
||||
<>
|
||||
{responseStatus === JSResponseState.NoResponse && (
|
||||
<NoResponse
|
||||
isRunDisabled={disabled}
|
||||
isRunning={isLoading}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
onRunClick={onButtonClick}
|
||||
/>
|
||||
)}
|
||||
{responseStatus === JSResponseState.IsExecuting && (
|
||||
<LoadingOverlayScreen theme={theme}>
|
||||
{createMessage(EXECUTING_FUNCTION)}
|
||||
</LoadingOverlayScreen>
|
||||
)}
|
||||
{responseStatus === JSResponseState.NoReturnValue && (
|
||||
<NoReturnValueWrapper>
|
||||
<Text kind="body-m">
|
||||
{createMessage(
|
||||
NO_JS_FUNCTION_RETURN_VALUE,
|
||||
currentFunction?.name,
|
||||
)}
|
||||
</Text>
|
||||
</NoReturnValueWrapper>
|
||||
)}
|
||||
{responseStatus === JSResponseState.ShowResponse && (
|
||||
<ReadOnlyEditor folding height="100%" input={response} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!localExecutionAllowed && (
|
||||
<JSRemoteExecutionView collectionData={jsCollectionData} />
|
||||
)}
|
||||
{responseStatus === JSResponseState.IsUpdating && (
|
||||
<LoadingOverlayScreen theme={theme}>
|
||||
{createMessage(UPDATING_JS_COLLECTION)}
|
||||
</LoadingOverlayScreen>
|
||||
)}
|
||||
</>
|
||||
</Flex>
|
||||
</ResponseTabWrapper>
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
currentFunction?.name,
|
||||
disabled,
|
||||
errors.length,
|
||||
hasExecutionParseErrors,
|
||||
isLoading,
|
||||
jsCollectionData,
|
||||
localExecutionAllowed,
|
||||
onButtonClick,
|
||||
theme,
|
||||
response,
|
||||
responseStatus,
|
||||
]);
|
||||
|
||||
const ideViewMode = useSelector(getIDEViewMode);
|
||||
const location = useLocation();
|
||||
|
||||
const tabs: BottomTab[] = [
|
||||
{
|
||||
key: DEBUGGER_TAB_KEYS.RESPONSE_TAB,
|
||||
title: createMessage(DEBUGGER_RESPONSE),
|
||||
panelComponent: (
|
||||
<>
|
||||
{localExecutionAllowed && hasExecutionParseErrors && (
|
||||
<ResponseErrorContainer>
|
||||
<ResponseErrorContent>
|
||||
<div className="t--js-response-parse-error-call-out">
|
||||
Function failed to execute. Check logs for more information.
|
||||
</div>
|
||||
</ResponseErrorContent>
|
||||
</ResponseErrorContainer>
|
||||
)}
|
||||
<ResponseTabWrapper
|
||||
className={errors.length && localExecutionAllowed ? "disable" : ""}
|
||||
>
|
||||
<Flex px="spaces-7" width="100%">
|
||||
<>
|
||||
{localExecutionAllowed && (
|
||||
<>
|
||||
{responseStatus === JSResponseState.NoResponse && (
|
||||
<NoResponse
|
||||
isRunDisabled={disabled}
|
||||
isRunning={isLoading}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
onRunClick={onButtonClick}
|
||||
/>
|
||||
)}
|
||||
{responseStatus === JSResponseState.IsExecuting && (
|
||||
<LoadingOverlayScreen theme={props.theme}>
|
||||
{createMessage(EXECUTING_FUNCTION)}
|
||||
</LoadingOverlayScreen>
|
||||
)}
|
||||
{responseStatus === JSResponseState.NoReturnValue && (
|
||||
<NoReturnValueWrapper>
|
||||
<Text kind="body-m">
|
||||
{createMessage(
|
||||
NO_JS_FUNCTION_RETURN_VALUE,
|
||||
currentFunction?.name,
|
||||
)}
|
||||
</Text>
|
||||
</NoReturnValueWrapper>
|
||||
)}
|
||||
{responseStatus === JSResponseState.ShowResponse && (
|
||||
<ReadOnlyEditor
|
||||
folding
|
||||
height={"100%"}
|
||||
input={{
|
||||
value: response as string,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!localExecutionAllowed && (
|
||||
<JSRemoteExecutionView collectionData={jsCollectionData} />
|
||||
)}
|
||||
{responseStatus === JSResponseState.IsUpdating && (
|
||||
<LoadingOverlayScreen theme={props.theme}>
|
||||
{createMessage(UPDATING_JS_COLLECTION)}
|
||||
</LoadingOverlayScreen>
|
||||
)}
|
||||
</>
|
||||
</Flex>
|
||||
</ResponseTabWrapper>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: DEBUGGER_TAB_KEYS.LOGS_TAB,
|
||||
title: createMessage(DEBUGGER_LOGS),
|
||||
panelComponent: <DebuggerLogs />,
|
||||
},
|
||||
];
|
||||
const ideType = getIDETypeByUrl(location.pathname);
|
||||
|
||||
if (ideViewMode === EditorViewMode.FullScreen) {
|
||||
tabs.push({
|
||||
key: DEBUGGER_TAB_KEYS.ERROR_TAB,
|
||||
title: createMessage(DEBUGGER_ERRORS),
|
||||
count: errorCount,
|
||||
panelComponent: <ErrorLogs />,
|
||||
});
|
||||
}
|
||||
const tabs = useMemo(() => {
|
||||
const jsTabs: BottomTab[] = [
|
||||
{
|
||||
key: DEBUGGER_TAB_KEYS.RESPONSE_TAB,
|
||||
title: createMessage(DEBUGGER_RESPONSE),
|
||||
panelComponent: JSResponseTab,
|
||||
},
|
||||
{
|
||||
key: DEBUGGER_TAB_KEYS.LOGS_TAB,
|
||||
title: createMessage(DEBUGGER_LOGS),
|
||||
panelComponent: <DebuggerLogs />,
|
||||
},
|
||||
];
|
||||
|
||||
if (ideViewMode === EditorViewMode.FullScreen) {
|
||||
jsTabs.push({
|
||||
key: DEBUGGER_TAB_KEYS.ERROR_TAB,
|
||||
title: createMessage(DEBUGGER_ERRORS),
|
||||
count: errorCount,
|
||||
panelComponent: <ErrorLogs />,
|
||||
});
|
||||
|
||||
if (ideType === IDE_TYPE.App) {
|
||||
jsTabs.push({
|
||||
key: DEBUGGER_TAB_KEYS.STATE_TAB,
|
||||
title: createMessage(DEBUGGER_STATE),
|
||||
panelComponent: <StateInspector />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return jsTabs;
|
||||
}, [JSResponseTab, errorCount, ideType, ideViewMode]);
|
||||
|
||||
// get the selected tab from the store.
|
||||
const { open, responseTabHeight, selectedTab } = useSelector(
|
||||
|
|
@ -217,18 +254,24 @@ function JSResponseView(props: Props) {
|
|||
);
|
||||
|
||||
// set the selected tab in the store.
|
||||
const setSelectedResponseTab = useCallback((selectedTab: string) => {
|
||||
dispatch(setJsPaneDebuggerState({ open: true, selectedTab }));
|
||||
}, []);
|
||||
const setSelectedResponseTab = useCallback(
|
||||
(selectedTab: string) => {
|
||||
dispatch(setJsPaneDebuggerState({ open: true, selectedTab }));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
// set the height of the response pane on resize.
|
||||
const setResponseHeight = useCallback((height: number) => {
|
||||
dispatch(setJsPaneDebuggerState({ responseTabHeight: height }));
|
||||
}, []);
|
||||
const setResponseHeight = useCallback(
|
||||
(height: number) => {
|
||||
dispatch(setJsPaneDebuggerState({ responseTabHeight: height }));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
// close the debugger
|
||||
const onToggle = useCallback(
|
||||
() => dispatch(setJsPaneDebuggerState({ open: !open })),
|
||||
[open],
|
||||
[dispatch, open],
|
||||
);
|
||||
|
||||
// Do not render if header tab is selected in the bottom bar.
|
||||
|
|
@ -251,12 +294,4 @@ function JSResponseView(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const errorCount = state.ui.debugger.context.errorCount;
|
||||
|
||||
return {
|
||||
errorCount,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withRouter(JSResponseView));
|
||||
export default JSResponseView;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ import { PRIMARY_KEY, FOREIGN_KEY } from "constants/DatasourceEditorConstants";
|
|||
import { Icon } from "@appsmith/ads";
|
||||
import { getAssetUrl } from "ee/utils/airgapHelpers";
|
||||
import { importSvg } from "@appsmith/ads-old";
|
||||
import WidgetFactory from "WidgetProvider/factory";
|
||||
import WidgetTypeIcon from "pages/Editor/Explorer/Widgets/WidgetIcon";
|
||||
import type { WidgetType } from "constants/WidgetConstants";
|
||||
|
||||
const ApiIcon = importSvg(
|
||||
async () => import("assets/icons/menu/api-colored.svg"),
|
||||
|
|
@ -225,6 +228,7 @@ const EntityIconWrapper = styled.div<{
|
|||
justify-content: center;
|
||||
text-align: center;
|
||||
border-radius: var(--ads-v2-border-radius);
|
||||
|
||||
svg,
|
||||
img {
|
||||
height: 100% !important;
|
||||
|
|
@ -357,3 +361,13 @@ export function DefaultModuleIcon() {
|
|||
</EntityIcon>
|
||||
);
|
||||
}
|
||||
|
||||
export function WidgetIconByType(widgetType: WidgetType) {
|
||||
const { IconCmp } = WidgetFactory.getWidgetMethods(widgetType);
|
||||
|
||||
return IconCmp ? <IconCmp /> : <WidgetTypeIcon type={widgetType} />;
|
||||
}
|
||||
|
||||
export function GlobeIcon() {
|
||||
return <Icon name="global-line" size="md" />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,7 +125,6 @@ function Files() {
|
|||
parentEntityType={parentEntityType}
|
||||
searchKeyword={""}
|
||||
step={2}
|
||||
type={type}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { getJsCollectionByBaseId } from "ee/selectors/entitiesSelector";
|
|||
import type { AppState } from "ee/reducers";
|
||||
import type { JSCollection } from "entities/JSCollection";
|
||||
import { JsFileIconV2 } from "../ExplorerIcons";
|
||||
import type { PluginType } from "entities/Action";
|
||||
import { jsCollectionIdURL } from "ee/RouteBuilder";
|
||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||
import { useLocation } from "react-router";
|
||||
|
|
@ -26,7 +25,6 @@ interface ExplorerJSCollectionEntityProps {
|
|||
searchKeyword?: string;
|
||||
baseCollectionId: string;
|
||||
isActive: boolean;
|
||||
type: PluginType;
|
||||
parentEntityId: string;
|
||||
parentEntityType: ActionParentEntityTypeInterface;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { omit, isUndefined, isEmpty } from "lodash";
|
|||
import equal from "fast-deep-equal";
|
||||
import { ActionExecutionResizerHeight } from "PluginActionEditor/components/PluginActionResponse/constants";
|
||||
import { klona } from "klona";
|
||||
import type { GenericEntityItem } from "ee/entities/IDE/constants";
|
||||
|
||||
export const DefaultDebuggerContext = {
|
||||
scrollPosition: 0,
|
||||
|
|
@ -22,6 +23,7 @@ const initialState: DebuggerReduxState = {
|
|||
expandId: "",
|
||||
hideErrors: true,
|
||||
context: DefaultDebuggerContext,
|
||||
stateInspector: {},
|
||||
};
|
||||
|
||||
// check the last message from the current log and update the occurrence count
|
||||
|
|
@ -185,6 +187,17 @@ const debuggerReducer = createImmerReducer(initialState, {
|
|||
},
|
||||
};
|
||||
},
|
||||
[ReduxActionTypes.SET_DEBUGGER_STATE_INSPECTOR_SELECTED_ITEM]: (
|
||||
state: DebuggerReduxState,
|
||||
action: ReduxAction<GenericEntityItem>,
|
||||
): DebuggerReduxState => {
|
||||
return {
|
||||
...state,
|
||||
stateInspector: {
|
||||
selectedItem: action.payload,
|
||||
},
|
||||
};
|
||||
},
|
||||
// Resetting debugger state after env switch
|
||||
[ReduxActionTypes.SWITCH_ENVIRONMENT_SUCCESS]: () => {
|
||||
return klona(initialState);
|
||||
|
|
@ -198,6 +211,9 @@ export interface DebuggerReduxState {
|
|||
expandId: string;
|
||||
hideErrors: boolean;
|
||||
context: DebuggerContext;
|
||||
stateInspector: {
|
||||
selectedItem?: GenericEntityItem;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DebuggerContext {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
put,
|
||||
select,
|
||||
take,
|
||||
takeEvery,
|
||||
takeLatest,
|
||||
} from "redux-saga/effects";
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
|
@ -19,10 +18,7 @@ import {
|
|||
updateAction,
|
||||
updateActionData,
|
||||
} from "actions/pluginActionActions";
|
||||
import {
|
||||
handleExecuteJSFunctionSaga,
|
||||
makeUpdateJSCollection,
|
||||
} from "sagas/JSPaneSagas";
|
||||
import { handleExecuteJSFunctionSaga } from "sagas/JSPaneSagas";
|
||||
|
||||
import type { ApplicationPayload } from "entities/Application";
|
||||
import type { ReduxAction } from "ee/constants/ReduxActionConstants";
|
||||
|
|
@ -1665,6 +1661,5 @@ export function* watchPluginActionExecutionSagas() {
|
|||
executePageLoadActionsSaga,
|
||||
),
|
||||
takeLatest(ReduxActionTypes.PLUGIN_SOFT_REFRESH, softRefreshActionsSaga),
|
||||
takeEvery(ReduxActionTypes.EXECUTE_JS_UPDATES, makeUpdateJSCollection),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,10 +95,10 @@ import type { ActionDescription } from "ee/workers/Evaluation/fns";
|
|||
import { handleEvalWorkerRequestSaga } from "./EvalWorkerActionSagas";
|
||||
import { getAppsmithConfigs } from "ee/configs";
|
||||
import {
|
||||
executeJSUpdates,
|
||||
type actionDataPayload,
|
||||
type updateActionDataPayloadType,
|
||||
} from "actions/pluginActionActions";
|
||||
import { executeJSUpdates } from "actions/jsPaneActions";
|
||||
import { setEvaluatedActionSelectorField } from "actions/actionSelectorActions";
|
||||
import { waitForWidgetConfigBuild } from "./InitSagas";
|
||||
import { logDynamicTriggerExecution } from "ee/sagas/analyticsSaga";
|
||||
|
|
@ -545,6 +545,7 @@ interface BUFFERED_ACTION {
|
|||
hasBufferedAction: boolean;
|
||||
actionDataPayloadConsolidated: actionDataPayload[];
|
||||
}
|
||||
|
||||
export function evalQueueBuffer() {
|
||||
let canTake = false;
|
||||
let hasDebouncedHandleUpdate = false;
|
||||
|
|
|
|||
|
|
@ -928,5 +928,6 @@ export default function* root() {
|
|||
ReduxActionTypes.CREATE_NEW_JS_FROM_ACTION_CREATOR,
|
||||
handleCreateNewJSFromActionCreator,
|
||||
),
|
||||
takeEvery(ReduxActionTypes.EXECUTE_JS_UPDATES, makeUpdateJSCollection),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -184,3 +184,6 @@ export const getCanvasDebuggerState = createSelector(
|
|||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const getDebuggerStateInspectorSelectedItem = (state: AppState) =>
|
||||
state.ui.debugger.stateInspector.selectedItem;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user