feat: State Inspector (#38368)

This commit is contained in:
Hetu Nandu 2025-01-03 18:05:09 +05:30 committed by GitHub
parent 28d35ad903
commit c2e4e11eb3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 825 additions and 182 deletions

View File

@ -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"),

View File

@ -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

View File

@ -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;
}
}

View File

@ -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,
};
};

View File

@ -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,
});

View File

@ -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,

View File

@ -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;

View File

@ -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 = {

View File

@ -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";

View File

@ -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

View File

@ -24,7 +24,6 @@ export const JSListItem = (props: JSListItemProps) => {
parentEntityType={parentEntityType}
searchKeyword={""}
step={1}
type={item.type}
/>
</Flex>
);

View File

@ -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,

View File

@ -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 = !(

View File

@ -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");
});
});

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export { useStateInspectorItems } from "./useStateInspectorItems";

View File

@ -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 };
};

View File

@ -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 };
};

View File

@ -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 };
};

View File

@ -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 };
};

View File

@ -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];
};

View File

@ -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];
};

View File

@ -0,0 +1 @@
export { StateInspector } from "./StateInspector";

View File

@ -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}
`;

View File

@ -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[];
};

View File

@ -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",
};
};

View File

@ -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",
}

View File

@ -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;

View File

@ -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" />;
}

View File

@ -125,7 +125,6 @@ function Files() {
parentEntityType={parentEntityType}
searchKeyword={""}
step={2}
type={type}
/>
);
} else {

View File

@ -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;
}

View File

@ -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 {

View File

@ -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),
]);
}

View File

@ -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;

View File

@ -928,5 +928,6 @@ export default function* root() {
ReduxActionTypes.CREATE_NEW_JS_FROM_ACTION_CREATOR,
handleCreateNewJSFromActionCreator,
),
takeEvery(ReduxActionTypes.EXECUTE_JS_UPDATES, makeUpdateJSCollection),
]);
}

View File

@ -184,3 +184,6 @@ export const getCanvasDebuggerState = createSelector(
};
},
);
export const getDebuggerStateInspectorSelectedItem = (state: AppState) =>
state.ui.debugger.stateInspector.selectedItem;