feat: add action resp viz (#39690)
## Description Fixes #39554 _or_ Fixes `Issue URL` > [!WARNING] > _If no issue exists, please create an issue first, and check with the maintainers if the issue is valid._ ## Automation /ok-to-test tags="@tag.Sanity" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/13811044062> > Commit: 1a5b458e43a338ad74eb48908a16ce695a6f53e2 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=13811044062&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Sanity` > Spec: > <hr>Wed, 12 Mar 2025 12:57:34 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Added a new forward arrow icon for enhanced design consistency. - Expanded sidebar functionality to support an optional extra title button for additional actions. - Introduced a comprehensive visualization experience, including interactive components for generating, saving, and displaying visualizations, along with prompt inputs, suggestions, and a results view. - Enhanced action capabilities to support visualization data and debugging with a new visualization tab. - Enabled new release functionalities via an updated feature flag system. - **Style** - Refined sidebar title spacing and layout for improved presentation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
c417c3058b
commit
73ee6e9ba7
|
|
@ -8,6 +8,9 @@ const AddMoreIcon = importRemixIcon(
|
|||
const AddMoreFillIcon = importRemixIcon(
|
||||
async () => import("remixicon-react/AddCircleFillIcon"),
|
||||
);
|
||||
const ArrowGoForwardLineIcon = importRemixIcon(
|
||||
async () => import("remixicon-react/ArrowGoForwardLineIcon"),
|
||||
);
|
||||
const ArrowGoBackLineIcon = importRemixIcon(
|
||||
async () => import("remixicon-react/ArrowGoBackLineIcon"),
|
||||
);
|
||||
|
|
@ -1178,6 +1181,7 @@ const ICON_LOOKUP = {
|
|||
"add-line": AddLineIcon,
|
||||
"add-more": AddMoreIcon,
|
||||
"add-more-fill": AddMoreFillIcon,
|
||||
"arrow-go-forward": ArrowGoForwardLineIcon,
|
||||
"alert-fill": AlertFillIcon,
|
||||
"alert-line": AlertLineIcon,
|
||||
"align-center": AlignCenter,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const _Sidebar = (props: SidebarProps, ref: Ref<HTMLDivElement>) => {
|
|||
children,
|
||||
className,
|
||||
collapsible = "offcanvas",
|
||||
extraTitleButton,
|
||||
onEnter: onEnterProp,
|
||||
onEntered: onEnteredProp,
|
||||
onExit: onExitProp,
|
||||
|
|
@ -47,7 +48,7 @@ const _Sidebar = (props: SidebarProps, ref: Ref<HTMLDivElement>) => {
|
|||
};
|
||||
|
||||
const content = (
|
||||
<SidebarContent title={title}>
|
||||
<SidebarContent extraTitleButton={extraTitleButton} title={title}>
|
||||
{typeof children === "function"
|
||||
? children({ isAnimating, state })
|
||||
: children}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import clsx from "clsx";
|
||||
import React, { type Ref } from "react";
|
||||
import React, { type ReactNode, type Ref } from "react";
|
||||
|
||||
import { Flex } from "../../Flex";
|
||||
import { Text } from "../../Text";
|
||||
|
|
@ -9,6 +9,7 @@ import { useSidebar } from "./use-sidebar";
|
|||
|
||||
interface SidebarContentProps {
|
||||
title?: string;
|
||||
extraTitleButton?: ReactNode;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
|
@ -17,7 +18,7 @@ const _SidebarContent = (
|
|||
props: SidebarContentProps,
|
||||
ref: Ref<HTMLDivElement>,
|
||||
) => {
|
||||
const { children, className, title, ...rest } = props;
|
||||
const { children, className, extraTitleButton, title, ...rest } = props;
|
||||
const { isMobile, setState, state } = useSidebar();
|
||||
|
||||
return (
|
||||
|
|
@ -39,26 +40,29 @@ const _SidebarContent = (
|
|||
className={styles.sidebarTitle}
|
||||
fontWeight={500}
|
||||
lineClamp={1}
|
||||
textAlign="center"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
{!isMobile && (
|
||||
<Button
|
||||
className={styles.sidebarHeaderExpandButton}
|
||||
color="neutral"
|
||||
icon={
|
||||
state === "full-width"
|
||||
? "arrows-diagonal-minimize"
|
||||
: "arrows-diagonal-2"
|
||||
}
|
||||
onPress={() =>
|
||||
setState(state === "full-width" ? "expanded" : "full-width")
|
||||
}
|
||||
variant="ghost"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Flex>
|
||||
{extraTitleButton}
|
||||
{!isMobile && (
|
||||
<Button
|
||||
className={styles.sidebarHeaderExpandButton}
|
||||
color="neutral"
|
||||
icon={
|
||||
state === "full-width"
|
||||
? "arrows-diagonal-minimize"
|
||||
: "arrows-diagonal-2"
|
||||
}
|
||||
onPress={() =>
|
||||
setState(state === "full-width" ? "expanded" : "full-width")
|
||||
}
|
||||
variant="ghost"
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<div className={styles.sidebarContentInner}>{children}</div>
|
||||
</Flex>
|
||||
|
|
|
|||
|
|
@ -170,10 +170,7 @@
|
|||
}
|
||||
|
||||
.sidebarTitle {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 var(--sizing-14);
|
||||
margin: 0 var(--sizing-4);
|
||||
}
|
||||
|
||||
.sidebarHeaderExpandButton {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export interface SidebarProviderProps {
|
|||
export interface SidebarProps {
|
||||
variant?: "sidebar" | "floating" | "inset";
|
||||
collapsible?: "offcanvas" | "icon" | "none";
|
||||
extraTitleButton?: ReactNode;
|
||||
onEnter?: () => void;
|
||||
onExit?: () => void;
|
||||
onEntered?: () => void;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
import { Button, Flex } from "@appsmith/ads";
|
||||
import type { ActionResponse } from "api/ActionAPI";
|
||||
import { type Action } from "entities/Action";
|
||||
import React, { useState } from "react";
|
||||
import { parseActionResponse } from "../Response/utils";
|
||||
import { EmptyVisualization } from "./components/EmptyVisualization";
|
||||
import { LoadingOverlay } from "./components/LoadingOverlay";
|
||||
import { PromptInput } from "./components/PromptInput";
|
||||
import { Result } from "./components/Result";
|
||||
import { SuggestionButtons } from "./components/SuggestionButtons";
|
||||
import { useGenerateVisualization } from "./useGenerateVisualization";
|
||||
import { useSaveVisualization } from "./useSaveVisualization";
|
||||
import { ErrorBoundary } from "@sentry/react";
|
||||
|
||||
interface VisualizationProps {
|
||||
action: Action;
|
||||
actionResponse?: ActionResponse;
|
||||
}
|
||||
|
||||
const BOTTOM_BAR_HEIGHT = 37;
|
||||
|
||||
export const Visualization = (props: VisualizationProps) => {
|
||||
const { action, actionResponse } = props;
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const { response } = parseActionResponse(actionResponse);
|
||||
const generateVisualization = useGenerateVisualization(
|
||||
action.id,
|
||||
action.visualization?.result,
|
||||
);
|
||||
const saveVisualization = useSaveVisualization(action.id);
|
||||
|
||||
return (
|
||||
// TODO: Remove the hardcoded height
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
height={`calc(100% - ${BOTTOM_BAR_HEIGHT}px)`}
|
||||
position="relative"
|
||||
>
|
||||
<Flex
|
||||
borderBottom="1px solid var(--ads-v2-color-border-muted)"
|
||||
flexDirection="column"
|
||||
gap="spaces-3"
|
||||
padding="spaces-3"
|
||||
>
|
||||
<SuggestionButtons onApply={setPrompt} />
|
||||
<Flex gap="spaces-3">
|
||||
<PromptInput
|
||||
isDisabled={!response || saveVisualization.isLoading}
|
||||
isLoading={generateVisualization.isLoading}
|
||||
onChange={setPrompt}
|
||||
onSubmit={async () =>
|
||||
generateVisualization.execute(prompt, response)
|
||||
}
|
||||
value={prompt}
|
||||
/>
|
||||
<Button
|
||||
isDisabled={
|
||||
generateVisualization.isLoading ||
|
||||
saveVisualization.isLoading ||
|
||||
!generateVisualization.hasPrevious
|
||||
}
|
||||
isIconButton
|
||||
kind="secondary"
|
||||
onClick={generateVisualization.previous}
|
||||
size="md"
|
||||
startIcon="arrow-go-back"
|
||||
/>
|
||||
<Button
|
||||
isDisabled={
|
||||
generateVisualization.isLoading ||
|
||||
saveVisualization.isLoading ||
|
||||
!generateVisualization.hasNext
|
||||
}
|
||||
isIconButton
|
||||
kind="secondary"
|
||||
onClick={generateVisualization.next}
|
||||
size="md"
|
||||
startIcon="arrow-go-forward"
|
||||
/>
|
||||
<Button
|
||||
isDisabled={
|
||||
generateVisualization.isLoading ||
|
||||
saveVisualization.isLoading ||
|
||||
!generateVisualization.elements
|
||||
}
|
||||
isLoading={saveVisualization.isLoading}
|
||||
kind="secondary"
|
||||
onClick={async () =>
|
||||
generateVisualization.elements &&
|
||||
saveVisualization.execute(generateVisualization.elements)
|
||||
}
|
||||
size="md"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex flexDirection="column" flexGrow={1} position="relative">
|
||||
{generateVisualization.elements ? (
|
||||
<ErrorBoundary fallback="Visualization failed. Please try again.">
|
||||
<Result data={response} elements={generateVisualization.elements} />
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
<EmptyVisualization />
|
||||
)}
|
||||
{generateVisualization.isLoading && <LoadingOverlay />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Flex, Text } from "@appsmith/ads";
|
||||
import NoVisualizationSVG from "assets/images/no-visualization.svg";
|
||||
import React from "react";
|
||||
|
||||
export const EmptyVisualization = () => {
|
||||
return (
|
||||
<Flex
|
||||
alignItems="center"
|
||||
flexDirection="column"
|
||||
gap="spaces-7"
|
||||
height="100%"
|
||||
justifyContent="center"
|
||||
>
|
||||
<img alt="No visualization" src={NoVisualizationSVG} />
|
||||
<Text>The response visualization will be shown here</Text>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { Flex, Text } from "@appsmith/ads";
|
||||
import React from "react";
|
||||
|
||||
export const LoadingOverlay = () => {
|
||||
return (
|
||||
<Flex
|
||||
alignItems="center"
|
||||
backgroundColor="color-mix(in srgb, var(--ads-v2-color-gray-800) 80%, transparent);"
|
||||
bottom="0"
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
left="0"
|
||||
position="absolute"
|
||||
right="0"
|
||||
top="0"
|
||||
>
|
||||
<Text color="var(--ads-v2-color-white)" kind="heading-m">
|
||||
Generating visualization...
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { Button, Input } from "@appsmith/ads";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
interface PromptInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
isLoading: boolean;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
const PromptForm = styled.form`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: var(--ads-v2-spaces-3);
|
||||
`;
|
||||
|
||||
export const PromptInput = (props: PromptInputProps) => {
|
||||
const { isDisabled, isLoading, onChange, onSubmit, value } = props;
|
||||
|
||||
return (
|
||||
<PromptForm
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
isDisabled={isDisabled}
|
||||
onChange={onChange}
|
||||
placeholder="Describe the data visualisation you want"
|
||||
size="md"
|
||||
value={value}
|
||||
/>
|
||||
<Button
|
||||
isDisabled={isDisabled || !value}
|
||||
isIconButton
|
||||
isLoading={isLoading}
|
||||
kind="primary"
|
||||
size="md"
|
||||
startIcon="arrow-up-line"
|
||||
type="submit"
|
||||
/>
|
||||
</PromptForm>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
/* eslint-disable react-perf/jsx-no-new-function-as-prop */
|
||||
import { transform } from "@babel/standalone";
|
||||
import type { VisualizationElements } from "entities/Action";
|
||||
import React, { memo, useEffect, useRef, useState } from "react";
|
||||
// @ts-expect-error - Type error due to raw loader
|
||||
import customWidgetScript from "!!raw-loader!../../../../../../widgets/CustomWidget/component/customWidgetscript.js";
|
||||
// @ts-expect-error - Type error due to raw loader
|
||||
import appsmithConsole from "!!raw-loader!../../../../../../widgets/CustomWidget/component/appsmithConsole.js";
|
||||
import { EVENTS } from "../../../../../../widgets/CustomWidget/component/customWidgetscript";
|
||||
|
||||
interface ResultProps {
|
||||
elements: VisualizationElements;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
const theme = {
|
||||
primaryColor: "#000000",
|
||||
backgroundColor: "#FFFFFF",
|
||||
borderRadius: 0,
|
||||
boxShadow: "none",
|
||||
fontFamily: "Arial",
|
||||
};
|
||||
|
||||
export const Result = memo(({ data, elements }: ResultProps) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const [isIframeReady, setIsIframeReady] = useState(false);
|
||||
|
||||
const compiledJs = transform(elements.js, {
|
||||
sourceType: "module",
|
||||
presets: ["react"],
|
||||
targets: {
|
||||
esmodules: true,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(
|
||||
function setupIframeMessageHandler() {
|
||||
const handler = (event: MessageEvent) => {
|
||||
const iframeWindow =
|
||||
iframeRef.current?.contentWindow ||
|
||||
iframeRef.current?.contentDocument?.defaultView;
|
||||
|
||||
if (event.source === iframeWindow) {
|
||||
iframeRef.current?.contentWindow?.postMessage(
|
||||
{
|
||||
type: EVENTS.CUSTOM_WIDGET_MESSAGE_RECEIVED_ACK,
|
||||
key: event.data.key,
|
||||
success: true,
|
||||
},
|
||||
"*",
|
||||
);
|
||||
|
||||
const message = event.data;
|
||||
|
||||
switch (message.type) {
|
||||
case EVENTS.CUSTOM_WIDGET_READY:
|
||||
setIsIframeReady(true);
|
||||
iframeRef.current?.contentWindow?.postMessage(
|
||||
{
|
||||
type: EVENTS.CUSTOM_WIDGET_READY_ACK,
|
||||
model: {
|
||||
data,
|
||||
},
|
||||
ui: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
theme,
|
||||
},
|
||||
"*",
|
||||
);
|
||||
|
||||
break;
|
||||
case "CUSTOM_WIDGET_CONSOLE_EVENT":
|
||||
if (message.data.type === "error") {
|
||||
throw new Error(message.data.args);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handler, false);
|
||||
|
||||
return () => window.removeEventListener("message", handler, false);
|
||||
},
|
||||
[data],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function updateModel() {
|
||||
if (
|
||||
iframeRef.current &&
|
||||
iframeRef.current.contentWindow &&
|
||||
isIframeReady
|
||||
) {
|
||||
iframeRef.current.contentWindow.postMessage(
|
||||
{
|
||||
type: EVENTS.CUSTOM_WIDGET_MODEL_CHANGE,
|
||||
model: { model: data },
|
||||
},
|
||||
"*",
|
||||
);
|
||||
}
|
||||
},
|
||||
[data],
|
||||
);
|
||||
|
||||
const srcDoc = `
|
||||
<html>
|
||||
<head>
|
||||
<style>${elements.css}</style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript">${appsmithConsole}</script>
|
||||
<script type="module">
|
||||
${customWidgetScript}
|
||||
main();
|
||||
</script>
|
||||
<script type="module">
|
||||
${compiledJs.code}
|
||||
</script>
|
||||
${elements.html}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
return (
|
||||
<iframe
|
||||
data-testid="t--visualization-result"
|
||||
ref={iframeRef}
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
srcDoc={srcDoc}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { Button, Flex } from "@appsmith/ads";
|
||||
import React, { memo } from "react";
|
||||
|
||||
interface SuggestionButtonsProps {
|
||||
onApply: (suggestion: string) => void;
|
||||
}
|
||||
|
||||
const suggestions = [
|
||||
{
|
||||
label: "Table",
|
||||
text: "Create a table",
|
||||
},
|
||||
{
|
||||
label: "Chart",
|
||||
text: "Create a chart",
|
||||
},
|
||||
{
|
||||
label: "List",
|
||||
text: "Create a list",
|
||||
},
|
||||
{
|
||||
label: "Form",
|
||||
text: "Create a form",
|
||||
},
|
||||
];
|
||||
|
||||
export const SuggestionButtons = memo((props: SuggestionButtonsProps) => {
|
||||
const { onApply } = props;
|
||||
|
||||
return (
|
||||
<Flex gap="spaces-2">
|
||||
{suggestions.map((suggestion) => (
|
||||
<Button
|
||||
key={suggestion.label}
|
||||
kind="secondary"
|
||||
onClick={() => onApply(suggestion.text)}
|
||||
>
|
||||
{suggestion.label}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from "./Visualization";
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import ActionAPI from "api/ActionAPI";
|
||||
import type { VisualizationElements } from "entities/Action";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export const useGenerateVisualization = (
|
||||
actionId: string,
|
||||
elements?: VisualizationElements,
|
||||
) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const [results, setResults] = useState<VisualizationElements[]>(
|
||||
elements ? [elements] : [],
|
||||
);
|
||||
const [currIndex, setCurrIndex] = useState(0);
|
||||
|
||||
const clampIndex = useCallback(
|
||||
(index: number) => {
|
||||
return Math.max(0, Math.min(index, results.length - 1));
|
||||
},
|
||||
[results],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function updateCurrIndex() {
|
||||
setCurrIndex(clampIndex(results.length - 1));
|
||||
},
|
||||
[results, clampIndex, setCurrIndex],
|
||||
);
|
||||
|
||||
const execute = useCallback(
|
||||
async (prompt: string, data: unknown) => {
|
||||
setIsLoading(true);
|
||||
setHasError(false);
|
||||
|
||||
try {
|
||||
const response = await ActionAPI.generateVisualization(
|
||||
actionId,
|
||||
prompt,
|
||||
data,
|
||||
);
|
||||
|
||||
setResults((result) => [
|
||||
...result,
|
||||
{
|
||||
js: response.data.result.js,
|
||||
css: response.data.result.css,
|
||||
html: response.data.result.html,
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
setHasError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[actionId],
|
||||
);
|
||||
|
||||
const previous = useCallback(() => {
|
||||
setCurrIndex((i) => clampIndex(i - 1));
|
||||
}, [clampIndex, setCurrIndex]);
|
||||
|
||||
const next = useCallback(() => {
|
||||
setCurrIndex((i) => clampIndex(i + 1));
|
||||
}, [clampIndex, setCurrIndex]);
|
||||
|
||||
return {
|
||||
execute,
|
||||
isLoading,
|
||||
hasError,
|
||||
elements: results.length > 0 ? results[currIndex] : undefined,
|
||||
hasPrevious: currIndex > 0,
|
||||
hasNext: currIndex < results.length - 1,
|
||||
previous,
|
||||
next,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { updateActionProperty } from "actions/pluginActionActions";
|
||||
import ActionAPI from "api/ActionAPI";
|
||||
import type { VisualizationElements } from "entities/Action";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
export const useSaveVisualization = (actionId: string) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const execute = useCallback(
|
||||
async (elements: VisualizationElements) => {
|
||||
setIsLoading(true);
|
||||
setHasError(false);
|
||||
|
||||
try {
|
||||
const response = await ActionAPI.saveVisualization(actionId, elements);
|
||||
|
||||
dispatch(
|
||||
updateActionProperty({
|
||||
id: actionId,
|
||||
field: "visualization",
|
||||
value: response.data,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
setHasError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[actionId],
|
||||
);
|
||||
|
||||
return {
|
||||
execute,
|
||||
isLoading,
|
||||
hasError,
|
||||
};
|
||||
};
|
||||
|
|
@ -162,6 +162,49 @@ class ActionAPI extends API {
|
|||
return API.get(`${ActionAPI.url}/view`, { applicationId });
|
||||
}
|
||||
|
||||
static async generateVisualization(
|
||||
actionId: string,
|
||||
prompt: string,
|
||||
data: unknown,
|
||||
): Promise<
|
||||
AxiosPromise<{
|
||||
result: { html: string; css: string; js: string; error: string };
|
||||
}>
|
||||
> {
|
||||
return API.post(
|
||||
`${ActionAPI.url}/${actionId}/visualize`,
|
||||
{
|
||||
prompt,
|
||||
data,
|
||||
},
|
||||
{},
|
||||
{
|
||||
timeout: 60000, // 1 minute
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static async saveVisualization(
|
||||
actionId: string,
|
||||
elements: {
|
||||
html: string;
|
||||
css: string;
|
||||
js: string;
|
||||
},
|
||||
): Promise<
|
||||
AxiosPromise<
|
||||
ApiResponse<{
|
||||
result: { html: string; css: string; js: string; error: string };
|
||||
}>
|
||||
>
|
||||
> {
|
||||
return API.patch(`${ActionAPI.url}/${actionId}/visualize`, {
|
||||
existing: {
|
||||
result: elements,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static async fetchActionsByPageId(
|
||||
pageId: string,
|
||||
): Promise<AxiosPromise<ApiResponse<Action[]>>> {
|
||||
|
|
|
|||
1
app/client/src/assets/images/no-visualization.svg
Normal file
1
app/client/src/assets/images/no-visualization.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 17 KiB |
|
|
@ -54,6 +54,7 @@ export const FEATURE_FLAG = {
|
|||
"release_external_saas_plugins_enabled",
|
||||
release_tablev2_infinitescroll_enabled:
|
||||
"release_tablev2_infinitescroll_enabled",
|
||||
release_fn_calling_enabled: "release_fn_calling_enabled",
|
||||
} as const;
|
||||
|
||||
export type FeatureFlag = keyof typeof FEATURE_FLAG;
|
||||
|
|
@ -99,6 +100,7 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
|
|||
release_ads_entity_item_enabled: false,
|
||||
release_external_saas_plugins_enabled: false,
|
||||
release_tablev2_infinitescroll_enabled: false,
|
||||
release_fn_calling_enabled: false,
|
||||
};
|
||||
|
||||
export const AB_TESTING_EVENT_KEYS = {
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ export enum DEBUGGER_TAB_KEYS {
|
|||
ERROR_TAB = "ERROR_TAB",
|
||||
LOGS_TAB = "LOGS_TAB",
|
||||
STATE_TAB = "STATE_TAB",
|
||||
VISUALIZATION_TAB = "VISUALIZATION_TAB",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,12 @@ export interface StoredDatasource {
|
|||
datasourceConfiguration?: { url?: string };
|
||||
}
|
||||
|
||||
export interface VisualizationElements {
|
||||
css: string;
|
||||
js: string;
|
||||
html: string;
|
||||
}
|
||||
|
||||
export interface BaseAction {
|
||||
id: string;
|
||||
baseId: string;
|
||||
|
|
@ -157,6 +163,9 @@ export interface BaseAction {
|
|||
// will always be undefined for non js actions
|
||||
isMainJSCollection?: boolean;
|
||||
source?: ActionCreationSourceTypeEnum;
|
||||
visualization?: {
|
||||
result: VisualizationElements;
|
||||
};
|
||||
}
|
||||
|
||||
interface BaseApiAction extends BaseAction {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ export const getIsAnvilLayoutEnabled = (state: AppState) => {
|
|||
return selectFeatureFlagCheck(state, FEATURE_FLAG.release_anvil_enabled);
|
||||
};
|
||||
|
||||
export const getReleaseFnCallingEnabled = (state: AppState) => {
|
||||
return selectFeatureFlagCheck(state, FEATURE_FLAG.release_fn_calling_enabled);
|
||||
};
|
||||
|
||||
/**
|
||||
* A selector to verify if the current application is an Anvil application.
|
||||
* This is done by getting the layout system type of the current application (getLayoutSystemType)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export const AvailableFeaturesToOverride: FeatureFlag[] = [
|
|||
"release_anvil_enabled",
|
||||
"release_layout_conversion_enabled",
|
||||
"release_anvil_toggle_enabled",
|
||||
"release_fn_calling_enabled",
|
||||
];
|
||||
export type OverriddenFeatureFlags = Partial<Record<FeatureFlag, boolean>>;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user