feat: dynamic post body in api form
- add info icon - display helper text and placeholders dynamically
This commit is contained in:
parent
6856571643
commit
0bdb9bea4c
53
app/client/src/actions/queryPaneActions.ts
Normal file
53
app/client/src/actions/queryPaneActions.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants";
|
||||
import { RestAction } from "api/ActionAPI";
|
||||
|
||||
export const createQueryRequest = (payload: Partial<RestAction>) => {
|
||||
return {
|
||||
type: ReduxActionTypes.CREATE_QUERY_INIT,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteQuery = (payload: { id: string }) => {
|
||||
return {
|
||||
type: ReduxActionTypes.DELETE_QUERY_INIT,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteQuerySuccess = (payload: { id: string }) => {
|
||||
return {
|
||||
type: ReduxActionTypes.DELETE_QUERY_SUCCESS,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
||||
export const executeQuery = (payload: {
|
||||
action: RestAction;
|
||||
actionId: string;
|
||||
}) => {
|
||||
return {
|
||||
type: ReduxActionTypes.EXECUTE_QUERY_REQUEST,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
||||
export const initQueryPane = (
|
||||
pluginType: string,
|
||||
urlId?: string,
|
||||
): ReduxAction<{ pluginType: string; id?: string }> => {
|
||||
return {
|
||||
type: ReduxActionTypes.INIT_QUERY_PANE,
|
||||
payload: { id: urlId, pluginType },
|
||||
};
|
||||
};
|
||||
|
||||
export const changeQuery = (
|
||||
id: string,
|
||||
pluginType: string,
|
||||
): ReduxAction<{ id: string; pluginType: string }> => {
|
||||
return {
|
||||
type: ReduxActionTypes.QUERY_PANE_CHANGE,
|
||||
payload: { id, pluginType },
|
||||
};
|
||||
};
|
||||
|
|
@ -108,6 +108,10 @@ export interface ExecuteActionRequest extends APIRequest {
|
|||
paginationField?: PaginationField;
|
||||
}
|
||||
|
||||
export interface ExecuteQueryRequest extends APIRequest {
|
||||
action: Pick<RestAction, "id"> | Omit<RestAction, "id">;
|
||||
}
|
||||
|
||||
export interface ExecuteActionResponse extends ApiResponse {
|
||||
actionId: string;
|
||||
data: any;
|
||||
|
|
@ -200,6 +204,10 @@ class ActionAPI extends API {
|
|||
static moveAction(moveRequest: MoveActionRequest) {
|
||||
return API.put(ActionAPI.url + "/move", moveRequest);
|
||||
}
|
||||
|
||||
static executeQuery(executeAction: any): AxiosPromise<ActionApiResponse> {
|
||||
return API.post(ActionAPI.url + "/execute", executeAction);
|
||||
}
|
||||
}
|
||||
|
||||
export default ActionAPI;
|
||||
|
|
|
|||
3
app/client/src/assets/icons/menu/queries.svg
Normal file
3
app/client/src/assets/icons/menu/queries.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 0C12.418 0 16 1.79 16 4C16 6.21 12.418 8 8 8C3.582 8 0 6.21 0 4C0 1.79 3.582 0 8 0ZM16 6V9C16 11.21 12.418 13 8 13C3.582 13 0 11.21 0 9V6C0 8.21 3.582 10 8 10C12.418 10 16 8.21 16 6ZM16 11V14C16 16.21 12.418 18 8 18C3.582 18 0 16.21 0 14V11C0 13.21 3.582 15 8 15C12.418 15 16 13.21 16 11Z" fill="#2E3D49"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 422 B |
3
app/client/src/assets/icons/widget/slash.svg
Normal file
3
app/client/src/assets/icons/widget/slash.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="15" height="19" viewBox="0 0 15 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="1.19091" y1="18.263" x2="13.4305" y2="1.4123" stroke="#D0D7DD" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 195 B |
|
|
@ -76,7 +76,7 @@ export interface TextInputProps extends IInputGroupProps {
|
|||
/** Additional classname */
|
||||
className?: string;
|
||||
type?: string;
|
||||
refHandler?: (ref: HTMLInputElement | null) => void;
|
||||
refHandler?: any;
|
||||
noValidate?: boolean;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import cm from "codemirror";
|
|||
import styled from "styled-components";
|
||||
import "codemirror/lib/codemirror.css";
|
||||
import "codemirror/theme/monokai.css";
|
||||
|
||||
require("codemirror/mode/javascript/javascript");
|
||||
|
||||
const Wrapper = styled.div<{ height: number }>`
|
||||
|
|
@ -24,13 +25,14 @@ class CodeEditor extends React.Component<Props> {
|
|||
componentDidMount(): void {
|
||||
if (this.textArea.current) {
|
||||
const readOnly = !this.props.input.onChange;
|
||||
|
||||
this.editor = cm.fromTextArea(this.textArea.current, {
|
||||
mode: { name: "javascript", json: true },
|
||||
value: this.props.input.value,
|
||||
readOnly,
|
||||
lineNumbers: true,
|
||||
tabSize: 2,
|
||||
indentWithTabs: true,
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
});
|
||||
this.editor.setSize(null, this.props.height);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import "codemirror/addon/hint/javascript-hint";
|
|||
import "codemirror/addon/display/placeholder";
|
||||
import "codemirror/addon/edit/closebrackets";
|
||||
import "codemirror/addon/display/autorefresh";
|
||||
import "codemirror/addon/mode/multiplex";
|
||||
import { getDataTreeForAutocomplete } from "selectors/dataTreeSelectors";
|
||||
import { AUTOCOMPLETE_MATCH_REGEX } from "constants/BindingsConstants";
|
||||
import ErrorTooltip from "components/editorComponents/ErrorTooltip";
|
||||
|
|
@ -21,6 +22,42 @@ import { DataTree } from "entities/DataTree/dataTreeFactory";
|
|||
import { Theme } from "constants/DefaultTheme";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
require("codemirror/mode/javascript/javascript");
|
||||
require("codemirror/mode/sql/sql");
|
||||
require("codemirror/addon/hint/sql-hint");
|
||||
|
||||
CodeMirror.defineMode("sql-js", function(config) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
return CodeMirror.multiplexingMode(
|
||||
CodeMirror.getMode(config, "text/x-sql"),
|
||||
{
|
||||
open: "{{",
|
||||
close: "}}",
|
||||
mode: CodeMirror.getMode(config, {
|
||||
name: "javascript",
|
||||
globalVars: true,
|
||||
}),
|
||||
},
|
||||
// .. more multiplexed styles can follow here
|
||||
);
|
||||
});
|
||||
|
||||
CodeMirror.defineMode("js-js", function(config) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
return CodeMirror.multiplexingMode(
|
||||
CodeMirror.getMode(config, { name: "javascript", json: true }),
|
||||
{
|
||||
open: "{{",
|
||||
close: "}}",
|
||||
mode: CodeMirror.getMode(config, {
|
||||
name: "javascript",
|
||||
globalVars: true,
|
||||
}),
|
||||
},
|
||||
// .. more multiplexed styles can follow here
|
||||
);
|
||||
});
|
||||
|
||||
const getBorderStyle = (
|
||||
props: { theme: Theme } & {
|
||||
|
|
@ -208,9 +245,12 @@ export type DynamicAutocompleteInputProps = {
|
|||
showLineNumbers?: boolean;
|
||||
allowTabIndent?: boolean;
|
||||
singleLine: boolean;
|
||||
disabled?: boolean;
|
||||
mode?: string | object;
|
||||
className?: string;
|
||||
leftImage?: string;
|
||||
disabled?: boolean;
|
||||
link?: string;
|
||||
baseMode?: string | object;
|
||||
};
|
||||
|
||||
type Props = ReduxStateProps &
|
||||
|
|
@ -249,7 +289,7 @@ class DynamicAutocompleteInput extends Component<Props, State> {
|
|||
};
|
||||
if (!this.props.allowTabIndent) extraKeys["Tab"] = false;
|
||||
this.editor = CodeMirror.fromTextArea(this.textArea.current, {
|
||||
mode: { name: "javascript", globalVars: true },
|
||||
mode: this.props.mode || { name: "javascript", globalVars: true },
|
||||
viewportMargin: 10,
|
||||
tabSize: 2,
|
||||
indentWithTabs: true,
|
||||
|
|
@ -259,6 +299,7 @@ class DynamicAutocompleteInput extends Component<Props, State> {
|
|||
autoCloseBrackets: true,
|
||||
...options,
|
||||
});
|
||||
|
||||
this.editor.on("change", _.debounce(this.handleChange, 300));
|
||||
this.editor.on("keyup", this.handleAutocompleteVisibility);
|
||||
this.editor.on("focus", this.handleEditorFocus);
|
||||
|
|
@ -286,6 +327,7 @@ class DynamicAutocompleteInput extends Component<Props, State> {
|
|||
if (this.editor) {
|
||||
this.editor.refresh();
|
||||
if (!this.state.isFocused) {
|
||||
const currentMode = this.editor.getOption("mode");
|
||||
const editorValue = this.editor.getValue();
|
||||
let inputValue = this.props.input.value;
|
||||
// Safe update of value of the editor when value updated outside the editor
|
||||
|
|
@ -295,6 +337,10 @@ class DynamicAutocompleteInput extends Component<Props, State> {
|
|||
if ((!!inputValue || inputValue === "") && inputValue !== editorValue) {
|
||||
this.editor.setValue(inputValue);
|
||||
}
|
||||
|
||||
if (currentMode !== this.props.mode) {
|
||||
this.editor.setOption("mode", this.props?.mode);
|
||||
}
|
||||
} else {
|
||||
// Update the dynamic bindings for autocomplete
|
||||
if (prevProps.dynamicData !== this.props.dynamicData) {
|
||||
|
|
@ -367,6 +413,11 @@ class DynamicAutocompleteInput extends Component<Props, State> {
|
|||
!cm.state.completionActive &&
|
||||
AUTOCOMPLETE_CLOSE_KEY_CODES.indexOf(event.code) === -1;
|
||||
|
||||
if (this.props.baseMode) {
|
||||
// https://github.com/codemirror/CodeMirror/issues/5249#issue-295565980
|
||||
cm.doc.modeOption = this.props.baseMode;
|
||||
}
|
||||
|
||||
if (shouldShow) {
|
||||
AnalyticsUtil.logEvent("AUTO_COMPELTE_SHOW", {});
|
||||
this.setState({
|
||||
|
|
@ -398,7 +449,7 @@ class DynamicAutocompleteInput extends Component<Props, State> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { input, meta, theme, singleLine, disabled } = this.props;
|
||||
const { input, meta, theme, singleLine, disabled, className } = this.props;
|
||||
const hasError = !!(meta && meta.error);
|
||||
let showError = false;
|
||||
if (this.editor) {
|
||||
|
|
@ -413,6 +464,7 @@ class DynamicAutocompleteInput extends Component<Props, State> {
|
|||
singleLine={singleLine}
|
||||
isFocused={this.state.isFocused}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
>
|
||||
<HintStyles />
|
||||
<IconContainer>
|
||||
|
|
|
|||
|
|
@ -8,12 +8,15 @@ import {
|
|||
PAGE_LIST_EDITOR_URL,
|
||||
DATA_SOURCES_EDITOR_URL,
|
||||
DATA_SOURCES_EDITOR_ID_URL,
|
||||
QUERIES_EDITOR_URL,
|
||||
QUERIES_EDITOR_ID_URL,
|
||||
getCurlImportPageURL,
|
||||
API_EDITOR_URL_WITH_SELECTED_PAGE_ID,
|
||||
getProviderTemplatesURL,
|
||||
} from "constants/routes";
|
||||
|
||||
import WidgetSidebar from "pages/Editor/WidgetSidebar";
|
||||
import QuerySidebar from "pages/Editor/QuerySidebar";
|
||||
import DataSourceSidebar from "pages/Editor/DataSourceSidebar";
|
||||
import ApiSidebar from "pages/Editor/ApiSidebar";
|
||||
import PageListSidebar from "pages/Editor/PageListSidebar";
|
||||
|
|
@ -73,11 +76,23 @@ export const Sidebar = () => {
|
|||
component={ApiSidebar}
|
||||
name={"ApiSidebar"}
|
||||
/>
|
||||
<AppRoute
|
||||
exact
|
||||
path={QUERIES_EDITOR_URL()}
|
||||
component={QuerySidebar}
|
||||
name={"QuerySidebar"}
|
||||
/>
|
||||
<AppRoute
|
||||
exact
|
||||
path={QUERIES_EDITOR_ID_URL()}
|
||||
component={QuerySidebar}
|
||||
name={"QuerySidebar"}
|
||||
/>
|
||||
<AppRoute
|
||||
exact
|
||||
path={DATA_SOURCES_EDITOR_URL()}
|
||||
name="DataSourceSidebar"
|
||||
component={DataSourceSidebar}
|
||||
name="DataSourceSidebar"
|
||||
/>
|
||||
<AppRoute
|
||||
exact
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ export const API_REQUEST_HEADERS: APIHeaders = {
|
|||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
export const POSTMAN = "POSTMAN";
|
||||
export const CURL = "CURL";
|
||||
export const Swagger = "Swagger";
|
||||
|
||||
export const OAuthURL = "/oauth2/authorization";
|
||||
export const GoogleOAuthURL = `${OAuthURL}/google`;
|
||||
export const GithubOAuthURL = `${OAuthURL}/github`;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export const DEFAULT_API_ACTION: Partial<RestAction> = {
|
|||
},
|
||||
};
|
||||
|
||||
export const API_CONSTANT = "API";
|
||||
export const DEFAULT_PROVIDER_OPTION = "Business Software";
|
||||
export const POST_BODY_FORMATS = ["application/json", "x-www-form-urlencoded"];
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ export const ReduxActionTypes: { [key: string]: string } = {
|
|||
LOAD_API_RESPONSE: "LOAD_API_RESPONSE",
|
||||
LOAD_QUERY_RESPONSE: "LOAD_QUERY_RESPONSE",
|
||||
RUN_API_REQUEST: "RUN_API_REQUEST",
|
||||
INIT_API_PANE: "INIT_API_PANE",
|
||||
API_PANE_CHANGE_API: "API_PANE_CHANGE_API",
|
||||
RUN_API_SUCCESS: "RUN_API_SUCCESS",
|
||||
EXECUTE_ACTION: "EXECUTE_ACTION",
|
||||
EXECUTE_ACTION_SUCCESS: "EXECUTE_ACTION_SUCCESS",
|
||||
|
|
@ -57,6 +59,7 @@ export const ReduxActionTypes: { [key: string]: string } = {
|
|||
UPDATE_ACTION_SUCCESS: "UPDATE_ACTION_SUCCESS",
|
||||
DELETE_ACTION_INIT: "DELETE_ACTION_INIT",
|
||||
DELETE_ACTION_SUCCESS: "DELETE_ACTION_SUCCESS",
|
||||
CREATE_QUERY_INIT: "CREATE_QUERY_INIT",
|
||||
FETCH_DATASOURCES_INIT: "FETCH_DATASOURCES_INIT",
|
||||
FETCH_DATASOURCES_SUCCESS: "FETCH_DATASOURCES_SUCCESS",
|
||||
CREATE_DATASOURCE_INIT: "CREATE_DATASOURCE_INIT",
|
||||
|
|
@ -86,11 +89,11 @@ export const ReduxActionTypes: { [key: string]: string } = {
|
|||
CREATE_APPLICATION_SUCCESS: "CREATE_APPLICATION_SUCCESS",
|
||||
UPDATE_WIDGET_PROPERTY_VALIDATION: "UPDATE_WIDGET_PROPERTY_VALIDATION",
|
||||
HIDE_PROPERTY_PANE: "HIDE_PROPERTY_PANE",
|
||||
INIT_API_PANE: "INIT_API_PANE",
|
||||
API_PANE_CHANGE_API: "API_PANE_CHANGE_API",
|
||||
INIT_DATASOURCE_PANE: "INIT_DATASOURCE_PANE",
|
||||
INIT_QUERY_PANE: "INIT_QUERY_PANE",
|
||||
UPDATE_API_DRAFT: "UPDATE_API_DRAFT",
|
||||
DELETE_API_DRAFT: "DELETE_API_DRAFT",
|
||||
QUERY_PANE_CHANGE: "QUERY_PANE_CHANGE",
|
||||
UPDATE_ROUTES_PARAMS: "UPDATE_ROUTES_PARAMS",
|
||||
PERSIST_USER_SESSION: "PERSIST_USER_SESSION",
|
||||
LOGIN_USER_INIT: "LOGIN_USER_INIT",
|
||||
|
|
@ -175,6 +178,11 @@ export const ReduxActionTypes: { [key: string]: string } = {
|
|||
FETCH_PROVIDER_TEMPLATES_SUCCESS: "FETCH_PROVIDER_TEMPLATES_SUCCESS",
|
||||
ADD_API_TO_PAGE_INIT: "ADD_API_TO_PAGE_INIT",
|
||||
ADD_API_TO_PAGE_SUCCESS: "ADD_API_TO_PAGE_SUCCESS",
|
||||
DELETE_QUERY_INIT: "DELETE_QUERY_INIT",
|
||||
DELETE_QUERY_SUCCESS: "DELETE_QUERY_SUCCESS",
|
||||
EXECUTE_QUERY_REQUEST: "EXECUTE_QUERY_REQUEST",
|
||||
RUN_QUERY_SUCCESS: "RUN_QUERY_SUCCESS",
|
||||
CLEAR_PREVIOUSLY_EXECUTED_QUERY: "CLEAR_PREVIOUSLY_EXECUTED_QUERY",
|
||||
FETCH_PROVIDERS_CATEGORIES_INIT: "FETCH_PROVIDERS_CATEGORIES_INIT",
|
||||
FETCH_PROVIDERS_CATEGORIES_SUCCESS: "FETCH_PROVIDERS_CATEGORIES_SUCCESS",
|
||||
FETCH_PROVIDERS_WITH_CATEGORY_INIT: "FETCH_PROVIDERS_WITH_CATEGORY_INIT",
|
||||
|
|
@ -260,6 +268,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
|
|||
MOVE_ACTION_ERROR: "MOVE_ACTION_ERROR",
|
||||
COPY_ACTION_ERROR: "COPY_ACTION_ERROR",
|
||||
DELETE_PAGE_ERROR: "DELETE_PAGE_ERROR",
|
||||
RUN_QUERY_ERROR: "RUN_QUERY_ERROR",
|
||||
DELETE_APPLICATION_ERROR: "DELETE_APPLICATION_ERROR",
|
||||
SET_DEFAULT_APPLICATION_PAGE_ERROR: "SET_DEFAULT_APPLICATION_PAGE_ERROR",
|
||||
CREATE_ORGANIZATION_ERROR: "CREATE_ORGANIZATION_ERROR",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ export const CREATE_PASSWORD_FORM_NAME = "CreatePasswordForm";
|
|||
|
||||
export const CREATE_ORGANIZATION_FORM_NAME = "CreateOrganizationForm";
|
||||
export const CURL_IMPORT_FORM = "CurlImportForm";
|
||||
|
||||
export const QUERY_EDITOR_FORM_NAME = "QueryEditorForm";
|
||||
export const API_HOME_SCREEN_FORM = "APIHomeScreenForm";
|
||||
|
||||
export const DATASOURCE_DB_FORM = "DatasourceDBForm";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { MenuIcons } from "icons/MenuIcons";
|
||||
import { FeatureFlagEnum } from "utils/featureFlags";
|
||||
|
||||
export const BASE_URL = "/";
|
||||
export const ORG_URL = "/org";
|
||||
export const APPLICATIONS_URL = `/applications`;
|
||||
|
|
@ -29,6 +30,12 @@ export type ProviderViewerRouteParams = {
|
|||
providerId: string;
|
||||
};
|
||||
|
||||
export type QueryEditorRouteParams = {
|
||||
applicationId: string;
|
||||
pageId: string;
|
||||
queryId: string;
|
||||
};
|
||||
|
||||
export const BUILDER_BASE_URL = (applicationId = ":applicationId"): string =>
|
||||
`/applications/${applicationId}`;
|
||||
|
||||
|
|
@ -66,6 +73,17 @@ export const DATA_SOURCES_EDITOR_ID_URL = (
|
|||
): string =>
|
||||
`${DATA_SOURCES_EDITOR_URL(applicationId, pageId)}/${datasourceId}`;
|
||||
|
||||
export const QUERIES_EDITOR_URL = (
|
||||
applicationId = ":applicationId",
|
||||
pageId = ":pageId",
|
||||
): string => `${BUILDER_PAGE_URL(applicationId, pageId)}/queries`;
|
||||
|
||||
export const QUERIES_EDITOR_ID_URL = (
|
||||
applicationId = ":applicationId",
|
||||
pageId = ":pageId",
|
||||
queryId = ":queryId",
|
||||
): string => `${QUERIES_EDITOR_URL(applicationId, pageId)}/${queryId}`;
|
||||
|
||||
export const API_EDITOR_ID_URL = (
|
||||
applicationId = ":applicationId",
|
||||
pageId = ":pageId",
|
||||
|
|
@ -124,6 +142,17 @@ export const getProviderTemplatesURL = (
|
|||
providerId = ":providerId",
|
||||
): string => `${API_EDITOR_URL(applicationId, pageId)}/provider/${providerId}`;
|
||||
|
||||
export const QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID = (
|
||||
applicationId = ":applicationId",
|
||||
pageId = ":pageId",
|
||||
selectedPageId = ":importTo",
|
||||
): string => {
|
||||
return `${BUILDER_PAGE_URL(
|
||||
applicationId,
|
||||
pageId,
|
||||
)}/queries?importTo=${selectedPageId}`;
|
||||
};
|
||||
|
||||
export const EDITOR_ROUTES = [
|
||||
{
|
||||
icon: MenuIcons.WIDGETS_ICON,
|
||||
|
|
@ -139,6 +168,14 @@ export const EDITOR_ROUTES = [
|
|||
title: "APIs",
|
||||
exact: false,
|
||||
},
|
||||
{
|
||||
icon: MenuIcons.QUERIES_ICON,
|
||||
className: "t--nav-link-query-editor",
|
||||
path: QUERIES_EDITOR_URL,
|
||||
title: "Queries",
|
||||
exact: false,
|
||||
flag: FeatureFlagEnum.QueryPane,
|
||||
},
|
||||
{
|
||||
icon: MenuIcons.DATASOURCES_ICON,
|
||||
className: "t--nav-link-datasource-editor",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { ReactComponent as ApisIcon } from "assets/icons/menu/api.svg";
|
|||
import { ReactComponent as OrgIcon } from "assets/icons/menu/org.svg";
|
||||
import { ReactComponent as PagesIcon } from "assets/icons/menu/pages.svg";
|
||||
import { ReactComponent as DataSourcesIcon } from "assets/icons/menu/data-sources.svg";
|
||||
import { ReactComponent as QueriesIcon } from "assets/icons/menu/queries.svg";
|
||||
import { ReactComponent as HomepageIcon } from "assets/icons/menu/homepage.svg";
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
|
|
@ -36,6 +37,11 @@ export const MenuIcons: {
|
|||
<DataSourcesIcon />
|
||||
</IconWrapper>
|
||||
),
|
||||
QUERIES_ICON: (props: IconProps) => (
|
||||
<IconWrapper {...props}>
|
||||
<QueriesIcon />
|
||||
</IconWrapper>
|
||||
),
|
||||
HOMEPAGE_ICON: (props: IconProps) => (
|
||||
<IconWrapper {...props}>
|
||||
<HomepageIcon />
|
||||
|
|
|
|||
455
app/client/src/pages/Editor/QueryEditor/Form.tsx
Normal file
455
app/client/src/pages/Editor/QueryEditor/Form.tsx
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
import React, { useState, useRef, useEffect } from "react";
|
||||
import {
|
||||
reduxForm,
|
||||
InjectedFormProps,
|
||||
Field,
|
||||
FormSubmitHandler,
|
||||
} from "redux-form";
|
||||
import {
|
||||
GridComponent,
|
||||
ColumnsDirective,
|
||||
ColumnDirective,
|
||||
} from "@syncfusion/ej2-react-grids";
|
||||
import ReactJson from "react-json-view";
|
||||
import styled, { createGlobalStyle } from "styled-components";
|
||||
import { Popover } from "@blueprintjs/core";
|
||||
import history from "utils/history";
|
||||
import DynamicAutocompleteInput from "components/editorComponents/DynamicAutocompleteInput";
|
||||
import { DATA_SOURCES_EDITOR_URL } from "constants/routes";
|
||||
import TemplateMenu from "./TemplateMenu";
|
||||
import Spinner from "components/editorComponents/Spinner";
|
||||
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
|
||||
import Button from "components/editorComponents/Button";
|
||||
import FormRow from "components/editorComponents/FormRow";
|
||||
import TextField from "components/editorComponents/form/fields/TextField";
|
||||
import DropdownField from "components/editorComponents/form/fields/DropdownField";
|
||||
import { BaseButton } from "components/designSystems/blueprint/ButtonComponent";
|
||||
import { Datasource } from "api/DatasourcesApi";
|
||||
import { RestAction } from "api/ActionAPI";
|
||||
import { QUERY_EDITOR_FORM_NAME } from "constants/forms";
|
||||
import { PLUGIN_PACKAGE_POSTGRES } from "constants/QueryEditorConstants";
|
||||
import "@syncfusion/ej2-react-grids/styles/material.css";
|
||||
|
||||
const QueryFormContainer = styled.div`
|
||||
font-size: 20px;
|
||||
padding: 20px 32px;
|
||||
width: 100%;
|
||||
max-height: 93vh;
|
||||
a {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.textAreaStyles {
|
||||
border-radius: 4px;
|
||||
border: 1px solid #d0d7dd;
|
||||
font-size: 14px;
|
||||
height: calc(100vh / 3);
|
||||
}
|
||||
.statementTextArea {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #2e3d49;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
&& {
|
||||
.CodeMirror-lines {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.queryInput {
|
||||
max-width: 30%;
|
||||
padding-right: 10px;
|
||||
}
|
||||
span.bp3-popover-target {
|
||||
display: initial !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const ActionButtons = styled.div`
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
const ActionButton = styled(BaseButton)`
|
||||
&&& {
|
||||
max-width: 72px;
|
||||
margin: 0 5px;
|
||||
min-height: 30px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ResponseContainer = styled.div`
|
||||
margin-top: 20px;
|
||||
`;
|
||||
|
||||
const ResponseContent = styled.div`
|
||||
height: calc(
|
||||
100vh - (100vh / 3) - 150px - ${props => props.theme.headerHeight}
|
||||
);
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const DropdownSelect = styled.div`
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
const NoDataSourceContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 62px;
|
||||
flex: 1;
|
||||
.font18 {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
margin-bottom: 23px;
|
||||
font-size: 18px;
|
||||
color: #2e3d49;
|
||||
}
|
||||
`;
|
||||
|
||||
const TooltipStyles = createGlobalStyle`
|
||||
.helper-tooltip{
|
||||
width: 378px;
|
||||
.bp3-popover {
|
||||
height: 137px;
|
||||
max-width: 378px;
|
||||
box-shadow: none;
|
||||
display: inherit !important;
|
||||
.bp3-popover-arrow {
|
||||
display: block;
|
||||
fill: none;
|
||||
}
|
||||
.bp3-popover-arrow-fill {
|
||||
fill: #23292E;
|
||||
}
|
||||
.bp3-popover-content {
|
||||
padding: 15px;
|
||||
background-color: #23292E;
|
||||
color: #fff;
|
||||
text-align: left;
|
||||
border-radius: 4px;
|
||||
text-transform: initial;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.popoverBtn {
|
||||
float: right;
|
||||
margin-top: 25px;
|
||||
}
|
||||
.popuptext {
|
||||
padding-right: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const TableHeader = styled.div`
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
font-family: "DM Sans";
|
||||
color: #2e3d49;
|
||||
`;
|
||||
|
||||
const LoadingContainer = styled(CenteredWrapper)`
|
||||
height: 50%;
|
||||
`;
|
||||
|
||||
const StyledGridComponent = styled(GridComponent)`
|
||||
&&& {
|
||||
.e-altrow {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
.e-active {
|
||||
background: #cccccc;
|
||||
}
|
||||
.e-gridcontent {
|
||||
max-height: calc(
|
||||
100vh - (100vh / 3) - 150px - 49px -
|
||||
${props => props.theme.headerHeight}
|
||||
);
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type QueryFormProps = {
|
||||
isCreating: boolean;
|
||||
onDeleteClick: () => void;
|
||||
onSaveClick: () => void;
|
||||
onRunClick: () => void;
|
||||
createTemplate: (template: any, name: string) => void;
|
||||
onSubmit: FormSubmitHandler<RestAction>;
|
||||
isDeleting: boolean;
|
||||
allowSave: boolean;
|
||||
isSaving: boolean;
|
||||
isRunning: boolean;
|
||||
dataSources: Datasource[];
|
||||
DATASOURCES_OPTIONS: any;
|
||||
executedQueryData: any;
|
||||
applicationId: string;
|
||||
selectedPluginPackage: string;
|
||||
pageId: string;
|
||||
location: {
|
||||
state: any;
|
||||
};
|
||||
};
|
||||
|
||||
export type StateAndRouteProps = QueryFormProps;
|
||||
|
||||
type Props = StateAndRouteProps &
|
||||
InjectedFormProps<RestAction, StateAndRouteProps>;
|
||||
|
||||
const QueryEditorForm: React.FC<Props> = (props: Props) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
allowSave,
|
||||
isDeleting,
|
||||
isSaving,
|
||||
isRunning,
|
||||
onSaveClick,
|
||||
onRunClick,
|
||||
onDeleteClick,
|
||||
DATASOURCES_OPTIONS,
|
||||
pageId,
|
||||
applicationId,
|
||||
dataSources,
|
||||
executedQueryData,
|
||||
selectedPluginPackage,
|
||||
createTemplate,
|
||||
isCreating,
|
||||
} = props;
|
||||
|
||||
const [showTemplateMenu, setMenuVisibility] = useState(true);
|
||||
|
||||
const isSQL = selectedPluginPackage === PLUGIN_PACKAGE_POSTGRES;
|
||||
const isNewQuery = props.location.state?.newQuery ?? false;
|
||||
let queryOutput = { body: [{ "": "" }] };
|
||||
const inputEl = useRef<HTMLInputElement>();
|
||||
|
||||
if (executedQueryData) {
|
||||
if (isSQL && executedQueryData.body.length) {
|
||||
queryOutput = executedQueryData;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isNewQuery) {
|
||||
inputEl.current?.select();
|
||||
}
|
||||
}, [isNewQuery]);
|
||||
|
||||
if (isCreating) {
|
||||
return (
|
||||
<LoadingContainer>
|
||||
<Spinner size={30} />
|
||||
</LoadingContainer>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<QueryFormContainer>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormRow>
|
||||
<TextField
|
||||
name="name"
|
||||
placeholder="Query"
|
||||
className="queryInput"
|
||||
refHandler={inputEl}
|
||||
/>
|
||||
<DropdownSelect>
|
||||
<DropdownField
|
||||
placeholder="Datasource"
|
||||
name="datasource.id"
|
||||
options={DATASOURCES_OPTIONS}
|
||||
width={200}
|
||||
maxMenuHeight={200}
|
||||
/>
|
||||
</DropdownSelect>
|
||||
<ActionButtons>
|
||||
<ActionButton
|
||||
text="Delete"
|
||||
accent="error"
|
||||
loading={isDeleting}
|
||||
onClick={onDeleteClick}
|
||||
/>
|
||||
{dataSources.length === 0 ? (
|
||||
<>
|
||||
<TooltipStyles />
|
||||
<Popover
|
||||
autoFocus={true}
|
||||
canEscapeKeyClose={true}
|
||||
content="You don’t have a Data Source to run this query
|
||||
"
|
||||
position="bottom"
|
||||
defaultIsOpen={false}
|
||||
usePortal
|
||||
portalClassName="helper-tooltip"
|
||||
>
|
||||
<ActionButton
|
||||
text="Run"
|
||||
accent="primary"
|
||||
loading={isRunning}
|
||||
/>
|
||||
<div>
|
||||
<p className="popuptext">
|
||||
You don’t have a Data Source to run this query
|
||||
</p>
|
||||
<Button
|
||||
onClick={() =>
|
||||
history.push(
|
||||
DATA_SOURCES_EDITOR_URL(applicationId, pageId),
|
||||
)
|
||||
}
|
||||
text="Add Datasource"
|
||||
intent="primary"
|
||||
filled
|
||||
size="small"
|
||||
className="popoverBtn"
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
</>
|
||||
) : (
|
||||
<ActionButton
|
||||
text="Run"
|
||||
loading={isRunning}
|
||||
accent="secondary"
|
||||
onClick={onRunClick}
|
||||
/>
|
||||
)}
|
||||
<ActionButton
|
||||
text="Save"
|
||||
accent="primary"
|
||||
filled
|
||||
onClick={onSaveClick}
|
||||
loading={isSaving}
|
||||
disabled={!allowSave}
|
||||
/>
|
||||
</ActionButtons>
|
||||
</FormRow>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<p className="statementTextArea">Query Statement</p>
|
||||
{isSQL ? (
|
||||
<a
|
||||
href="https://www.postgresql.org/docs/12/index.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
PostgreSQL docs
|
||||
</a>
|
||||
) : (
|
||||
<a
|
||||
href="https://docs.mongodb.com/manual/reference/command/nav-crud/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Mongo docs
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
{isNewQuery && showTemplateMenu ? (
|
||||
<TemplateMenu
|
||||
createTemplate={templateString => {
|
||||
const name = isSQL
|
||||
? "actionConfiguration.query.cmd"
|
||||
: "actionConfiguration.query";
|
||||
|
||||
setMenuVisibility(false);
|
||||
createTemplate(templateString, name);
|
||||
}}
|
||||
selectedPluginPackage={selectedPluginPackage}
|
||||
/>
|
||||
) : isSQL ? (
|
||||
<Field
|
||||
name="actionConfiguration.query.cmd"
|
||||
component={DynamicAutocompleteInput}
|
||||
className="textAreaStyles"
|
||||
mode="sql-js"
|
||||
baseMode="text/x-sql"
|
||||
/>
|
||||
) : (
|
||||
<Field
|
||||
name="actionConfiguration.query"
|
||||
component={DynamicAutocompleteInput}
|
||||
className="textAreaStyles"
|
||||
mode="js-js"
|
||||
normalize={(value: any) => {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
return value;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
|
||||
{dataSources.length === 0 && (
|
||||
<NoDataSourceContainer>
|
||||
<p className="font18">
|
||||
Seems like you don’t have any Datasouces to create a query
|
||||
</p>
|
||||
<Button
|
||||
onClick={() =>
|
||||
history.push(DATA_SOURCES_EDITOR_URL(applicationId, pageId))
|
||||
}
|
||||
text="Add a Datasource"
|
||||
intent="primary"
|
||||
filled
|
||||
size="small"
|
||||
icon="plus"
|
||||
/>
|
||||
</NoDataSourceContainer>
|
||||
)}
|
||||
|
||||
{executedQueryData && dataSources.length && (
|
||||
<ResponseContainer>
|
||||
<p className="statementTextArea">Query response</p>
|
||||
<ResponseContent>
|
||||
{isSQL ? (
|
||||
<StyledGridComponent dataSource={queryOutput.body}>
|
||||
<ColumnsDirective>
|
||||
{Object.keys(queryOutput.body[0]).map((key: string) => {
|
||||
return (
|
||||
<ColumnDirective
|
||||
headerTemplate={(props: { headerText: any }) => {
|
||||
const { headerText } = props;
|
||||
|
||||
return <TableHeader>{headerText}</TableHeader>;
|
||||
}}
|
||||
key={key}
|
||||
field={key}
|
||||
width="200"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ColumnsDirective>
|
||||
</StyledGridComponent>
|
||||
) : (
|
||||
<ReactJson
|
||||
src={executedQueryData.body}
|
||||
name={null}
|
||||
enableClipboard={false}
|
||||
displayObjectSize={false}
|
||||
displayDataTypes={false}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ResponseContent>
|
||||
</ResponseContainer>
|
||||
)}
|
||||
</QueryFormContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default reduxForm<RestAction, StateAndRouteProps>({
|
||||
form: QUERY_EDITOR_FORM_NAME,
|
||||
enableReinitialize: true,
|
||||
})(QueryEditorForm);
|
||||
290
app/client/src/pages/Editor/QueryEditor/QueryHomeScreen.tsx
Normal file
290
app/client/src/pages/Editor/QueryEditor/QueryHomeScreen.tsx
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Icon, Card } from "@blueprintjs/core";
|
||||
import { connect } from "react-redux";
|
||||
import { AppState } from "reducers";
|
||||
import ImageAlt from "assets/images/placeholder-image.svg";
|
||||
import Postgres from "assets/images/Postgress.png";
|
||||
import MongoDB from "assets/images/MongoDB.png";
|
||||
import { createNewQueryName } from "utils/AppsmithUtils";
|
||||
import { Plugin } from "api/PluginApi";
|
||||
import {
|
||||
getPlugins,
|
||||
getPluginIdsOfPackageNames,
|
||||
} from "selectors/entitiesSelector";
|
||||
import { ActionDataState } from "reducers/entityReducers/actionsReducer";
|
||||
import { Datasource } from "api/DatasourcesApi";
|
||||
import { RestAction } from "api/ActionAPI";
|
||||
import history from "utils/history";
|
||||
import { createActionRequest } from "actions/actionActions";
|
||||
import { BaseTextInput } from "components/designSystems/appsmith/TextInputComponent";
|
||||
import { Colors } from "constants/Colors";
|
||||
import {
|
||||
PLUGIN_PACKAGE_MONGO,
|
||||
PLUGIN_PACKAGE_POSTGRES,
|
||||
PLUGIN_PACKAGE_DBS,
|
||||
} from "constants/QueryEditorConstants";
|
||||
import { Page } from "constants/ReduxActionConstants";
|
||||
import {
|
||||
QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID,
|
||||
QUERIES_EDITOR_ID_URL,
|
||||
} from "constants/routes";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
|
||||
const QueryHomePage = styled.div`
|
||||
font-size: 20px;
|
||||
padding: 20px;
|
||||
max-height: 95vh;
|
||||
overflow: auto;
|
||||
|
||||
.addIcon {
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.createText {
|
||||
font-size: 14px;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
letter-spacing: -0.17px;
|
||||
color: #2e3d49;
|
||||
font-weight: 500;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.textBtn {
|
||||
font-size: 14px;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
letter-spacing: -0.17px;
|
||||
color: #2e3d49;
|
||||
font-weight: 500;
|
||||
text-decoration: none !important;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
`;
|
||||
|
||||
const SearchContainer = styled.div`
|
||||
display: flex;
|
||||
width: 40%;
|
||||
`;
|
||||
|
||||
const SearchBar = styled(BaseTextInput)`
|
||||
margin-bottom: 10px;
|
||||
input {
|
||||
background-color: ${Colors.WHITE};
|
||||
1px solid ${Colors.GEYSER};
|
||||
}
|
||||
`;
|
||||
|
||||
const CardsWrapper = styled.div`
|
||||
flex: 1;
|
||||
display: -webkit-box;
|
||||
flex-wrap: wrap;
|
||||
-webkit-box-pack: start !important;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
min-width: 140px;
|
||||
border-radius: 4px;
|
||||
.createCard {
|
||||
margin-right: 20px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: 140px;
|
||||
height: 110px;
|
||||
padding-bottom: 0px;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const DatasourceCardsContainer = styled.div`
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
margin-left: -10px;
|
||||
justify-content: flex-start;
|
||||
text-align: center;
|
||||
min-width: 150px;
|
||||
border-radius: 4px;
|
||||
|
||||
.eachDatasourceCard {
|
||||
margin: 10px;
|
||||
width: 140px;
|
||||
height: 110px;
|
||||
padding-bottom: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dataSourceImage {
|
||||
height: 52px;
|
||||
width: auto;
|
||||
margin-top: -5px;
|
||||
max-width: 100%;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
`;
|
||||
|
||||
type QueryHomeScreenProps = {
|
||||
dataSources: Datasource[];
|
||||
applicationId: string;
|
||||
pageId: string;
|
||||
createAction: (data: Partial<RestAction>) => void;
|
||||
actions: ActionDataState;
|
||||
pluginIds: Array<string> | undefined;
|
||||
isCreating: boolean;
|
||||
location: {
|
||||
search: string;
|
||||
};
|
||||
history: {
|
||||
replace: (data: string) => void;
|
||||
push: (data: string) => void;
|
||||
};
|
||||
plugins: Plugin[];
|
||||
pages: Page[];
|
||||
};
|
||||
|
||||
class QueryHomeScreen extends React.Component<QueryHomeScreenProps> {
|
||||
handleCreateNewQuery = (dataSourceId: string, params: string) => {
|
||||
const { actions, pages, applicationId } = this.props;
|
||||
const pageId = new URLSearchParams(params).get("importTo");
|
||||
const page = pages.find(page => page.pageId === pageId);
|
||||
|
||||
AnalyticsUtil.logEvent("CREATE_QUERY_CLICK", {
|
||||
pageName: page?.pageName ?? "",
|
||||
});
|
||||
if (pageId) {
|
||||
const newQueryName = createNewQueryName(actions, pageId);
|
||||
|
||||
history.push(QUERIES_EDITOR_ID_URL(applicationId, pageId));
|
||||
this.props.createAction({
|
||||
name: newQueryName,
|
||||
pageId,
|
||||
datasource: {
|
||||
id: dataSourceId,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getImageSrc = (dataSource: Datasource) => {
|
||||
const { plugins } = this.props;
|
||||
const { pluginId } = dataSource;
|
||||
const plugin = plugins.find(
|
||||
(plugin: { id: string }) => plugin.id === pluginId,
|
||||
);
|
||||
|
||||
switch (plugin?.packageName) {
|
||||
case PLUGIN_PACKAGE_MONGO:
|
||||
return MongoDB;
|
||||
case PLUGIN_PACKAGE_POSTGRES:
|
||||
return Postgres;
|
||||
default:
|
||||
return ImageAlt;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
dataSources,
|
||||
pluginIds,
|
||||
applicationId,
|
||||
pageId,
|
||||
history,
|
||||
location,
|
||||
} = this.props;
|
||||
|
||||
const validDataSources: Array<Datasource> = [];
|
||||
dataSources.forEach(dataSource => {
|
||||
if (pluginIds?.includes(dataSource.pluginId)) {
|
||||
validDataSources.push(dataSource);
|
||||
}
|
||||
});
|
||||
|
||||
const queryParams: string = location.search;
|
||||
const destinationPageId = new URLSearchParams(location.search).get(
|
||||
"importTo",
|
||||
);
|
||||
|
||||
if (!destinationPageId) {
|
||||
history.push(
|
||||
QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID(applicationId, pageId, pageId),
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryHomePage>
|
||||
<SearchContainer>
|
||||
<SearchBar
|
||||
icon="search"
|
||||
input={{
|
||||
onChange: () => null,
|
||||
}}
|
||||
placeholder="Search"
|
||||
/>
|
||||
</SearchContainer>
|
||||
|
||||
<p style={{ fontSize: "14px" }}>Create Query</p>
|
||||
<CardsWrapper>
|
||||
{dataSources.length > 0 && (
|
||||
<>
|
||||
<DatasourceCardsContainer>
|
||||
<Card
|
||||
interactive={false}
|
||||
className="eachDatasourceCard"
|
||||
onClick={() =>
|
||||
this.handleCreateNewQuery(
|
||||
validDataSources[0].id,
|
||||
queryParams,
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon icon="plus" iconSize={25} className="addIcon" />
|
||||
<p className="createText">Blank Query</p>
|
||||
</Card>
|
||||
{validDataSources.map(dataSource => {
|
||||
return (
|
||||
<Card
|
||||
interactive={false}
|
||||
className="eachDatasourceCard"
|
||||
key={dataSource.id}
|
||||
onClick={() =>
|
||||
this.handleCreateNewQuery(dataSource.id, queryParams)
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={this.getImageSrc(dataSource)}
|
||||
className="dataSourceImage"
|
||||
alt="Datasource"
|
||||
></img>
|
||||
|
||||
<p className="textBtn" title={dataSource.name}>
|
||||
{dataSource.name}
|
||||
</p>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</DatasourceCardsContainer>
|
||||
</>
|
||||
)}
|
||||
</CardsWrapper>
|
||||
</QueryHomePage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => ({
|
||||
pluginIds: getPluginIdsOfPackageNames(state, PLUGIN_PACKAGE_DBS),
|
||||
plugins: getPlugins(state),
|
||||
actions: state.entities.actions,
|
||||
pages: state.entities.pageList.pages,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => ({
|
||||
createAction: (data: Partial<RestAction>) => {
|
||||
dispatch(createActionRequest(data));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueryHomeScreen);
|
||||
134
app/client/src/pages/Editor/QueryEditor/TemplateMenu.tsx
Normal file
134
app/client/src/pages/Editor/QueryEditor/TemplateMenu.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import Templates from "./Templates";
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
height: 185px;
|
||||
padding: 16px 24px;
|
||||
flex: 1;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #d0d7dd;
|
||||
flex-direction: column;
|
||||
color: #4e5d78;
|
||||
`;
|
||||
|
||||
const BulletPoint = styled.div`
|
||||
height: 4px;
|
||||
width: 4px;
|
||||
border-radius: 2px;
|
||||
background-color: #c4c4c4;
|
||||
`;
|
||||
|
||||
const Item = styled.div`
|
||||
font-size: 14px;
|
||||
line-height: 11px;
|
||||
margin-left: 6px;
|
||||
`;
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
padding: 9px;
|
||||
width: 108px;
|
||||
cursor: pointer;
|
||||
|
||||
:hover {
|
||||
background-color: #ebeff2;
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface TemplateMenuProps {
|
||||
createTemplate: (template: any) => void;
|
||||
selectedPluginPackage: string;
|
||||
}
|
||||
|
||||
type Props = TemplateMenuProps;
|
||||
|
||||
class TemplateMenu extends React.Component<Props> {
|
||||
nameInput!: HTMLDivElement | null;
|
||||
|
||||
componentDidMount() {
|
||||
this.nameInput?.focus();
|
||||
}
|
||||
|
||||
fetchTemplate = (queryType: React.ReactText) => {
|
||||
const { selectedPluginPackage } = this.props;
|
||||
const allTemplates = Templates[selectedPluginPackage];
|
||||
|
||||
if (allTemplates) {
|
||||
return allTemplates[queryType];
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { createTemplate } = this.props;
|
||||
|
||||
return (
|
||||
<Container
|
||||
ref={input => {
|
||||
this.nameInput = input;
|
||||
}}
|
||||
tabIndex={0}
|
||||
onKeyPress={e => {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.key === "Enter") {
|
||||
createTemplate("");
|
||||
}
|
||||
}}
|
||||
onClick={() => createTemplate("")}
|
||||
>
|
||||
<div style={{ fontSize: 14 }}>
|
||||
Press enter to start with a blank state or select a template.
|
||||
</div>
|
||||
<div style={{ marginTop: "6px" }}>
|
||||
<Row
|
||||
onClick={e => {
|
||||
const template = this.fetchTemplate("create");
|
||||
createTemplate(template);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<BulletPoint />
|
||||
<Item>Create</Item>
|
||||
</Row>
|
||||
<Row
|
||||
onClick={e => {
|
||||
const template = this.fetchTemplate("read");
|
||||
createTemplate(template);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<BulletPoint />
|
||||
<Item>Read</Item>
|
||||
</Row>
|
||||
<Row
|
||||
onClick={e => {
|
||||
const template = this.fetchTemplate("delete");
|
||||
createTemplate(template);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<BulletPoint />
|
||||
<Item>Delete</Item>
|
||||
</Row>
|
||||
<Row
|
||||
onClick={e => {
|
||||
const template = this.fetchTemplate("update");
|
||||
createTemplate(template);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<BulletPoint />
|
||||
<Item>Update</Item>
|
||||
</Row>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TemplateMenu;
|
||||
53
app/client/src/pages/Editor/QueryEditor/Templates.tsx
Normal file
53
app/client/src/pages/Editor/QueryEditor/Templates.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import {
|
||||
PLUGIN_PACKAGE_MONGO,
|
||||
PLUGIN_PACKAGE_POSTGRES,
|
||||
} from "constants/QueryEditorConstants";
|
||||
|
||||
const Templates: Record<string, any> = {
|
||||
[PLUGIN_PACKAGE_MONGO]: {
|
||||
create: {
|
||||
insert: "users",
|
||||
documents: [
|
||||
{
|
||||
name: "John Smith",
|
||||
email: ["john@appsmith.com](mailto:%22john@appsmith.com)"],
|
||||
gender: "M",
|
||||
},
|
||||
],
|
||||
},
|
||||
read: {
|
||||
find: "users",
|
||||
filter: { id: { $gte: 10 } },
|
||||
sort: { id: 1 },
|
||||
limit: 10,
|
||||
},
|
||||
delete: {
|
||||
delete: "users",
|
||||
deletes: [{ q: { id: 10 } }],
|
||||
},
|
||||
update: {
|
||||
update: "users",
|
||||
updates: [
|
||||
{
|
||||
q: { id: 10 },
|
||||
u: {
|
||||
name: "Updated Sam",
|
||||
email: ["updates@appsmith.com](mailto:%22updates@appsmith.com)"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
[PLUGIN_PACKAGE_POSTGRES]: {
|
||||
create: `INSERT INTO users(
|
||||
id, name, gender, avatar, email, address, role)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?);`,
|
||||
read: "SELECT * FROM users LIMIT 10 ORDER BY id",
|
||||
delete: `DELETE FROM users WHERE id=?`,
|
||||
update: `UPDATE users
|
||||
Set status='APPROVED'
|
||||
WHERE id=1;`,
|
||||
},
|
||||
};
|
||||
|
||||
export default Templates;
|
||||
194
app/client/src/pages/Editor/QueryEditor/index.tsx
Normal file
194
app/client/src/pages/Editor/QueryEditor/index.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import React from "react";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
submit,
|
||||
getFormValues,
|
||||
getFormInitialValues,
|
||||
change,
|
||||
} from "redux-form";
|
||||
import _ from "lodash";
|
||||
import styled from "styled-components";
|
||||
import { QueryEditorRouteParams } from "constants/routes";
|
||||
import QueryEditorForm from "./Form";
|
||||
import QueryHomeScreen from "./QueryHomeScreen";
|
||||
import { updateAction } from "actions/actionActions";
|
||||
import { deleteQuery, executeQuery } from "actions/queryPaneActions";
|
||||
import { AppState } from "reducers";
|
||||
import { getDataSources } from "selectors/editorSelectors";
|
||||
import { QUERY_EDITOR_FORM_NAME } from "constants/forms";
|
||||
import { Datasource } from "api/DatasourcesApi";
|
||||
import { RestAction } from "api/ActionAPI";
|
||||
import { QueryPaneReduxState } from "reducers/uiReducers/queryPaneReducer";
|
||||
import {
|
||||
getPluginIdsOfPackageNames,
|
||||
getPluginPackageFromDatasourceId,
|
||||
} from "selectors/entitiesSelector";
|
||||
import { PLUGIN_PACKAGE_DBS } from "constants/QueryEditorConstants";
|
||||
import { getCurrentApplication } from "selectors/applicationSelectors";
|
||||
import { ApiPaneReduxState } from "reducers/uiReducers/apiPaneReducer";
|
||||
|
||||
const EmptyStateContainer = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
font-size: 20px;
|
||||
`;
|
||||
|
||||
type QueryPageProps = {
|
||||
dataSources: Datasource[];
|
||||
queryPane: QueryPaneReduxState;
|
||||
formData: RestAction;
|
||||
isCreating: boolean;
|
||||
apiPane: ApiPaneReduxState;
|
||||
initialValues: RestAction;
|
||||
pluginIds: Array<string> | undefined;
|
||||
submitForm: (name: string) => void;
|
||||
createAction: (values: RestAction) => void;
|
||||
runAction: (action: RestAction, actionId: string) => void;
|
||||
deleteAction: (id: string) => void;
|
||||
updateAction: (data: RestAction) => void;
|
||||
createTemplate: (template: string) => void;
|
||||
executedQueryData: any;
|
||||
selectedPluginPackage: string;
|
||||
};
|
||||
|
||||
type StateAndRouteProps = RouteComponentProps<QueryEditorRouteParams>;
|
||||
|
||||
type Props = QueryPageProps & StateAndRouteProps;
|
||||
|
||||
class QueryEditor extends React.Component<Props> {
|
||||
handleSubmit = () => {
|
||||
const { formData } = this.props;
|
||||
this.props.updateAction(formData);
|
||||
};
|
||||
|
||||
handleSaveClick = () => {
|
||||
this.props.submitForm(QUERY_EDITOR_FORM_NAME);
|
||||
};
|
||||
|
||||
handleDeleteClick = () => {
|
||||
const { queryId } = this.props.match.params;
|
||||
this.props.deleteAction(queryId);
|
||||
};
|
||||
|
||||
handleRunClick = () => {
|
||||
const { formData, match } = this.props;
|
||||
const payload = { ...formData };
|
||||
|
||||
this.props.runAction(payload, match.params.queryId);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
dataSources,
|
||||
queryPane,
|
||||
createTemplate,
|
||||
match: {
|
||||
params: { queryId },
|
||||
},
|
||||
pluginIds,
|
||||
executedQueryData,
|
||||
selectedPluginPackage,
|
||||
apiPane,
|
||||
isCreating,
|
||||
} = this.props;
|
||||
const { applicationId, pageId } = this.props.match.params;
|
||||
|
||||
if (!pluginIds?.length) {
|
||||
return (
|
||||
<EmptyStateContainer>{"Plugin is not installed"}</EmptyStateContainer>
|
||||
);
|
||||
}
|
||||
const { drafts } = apiPane;
|
||||
const { isSaving, isRunning, isDeleting } = queryPane;
|
||||
|
||||
const validDataSources: Array<Datasource> = [];
|
||||
dataSources.forEach(dataSource => {
|
||||
if (pluginIds?.includes(dataSource.pluginId)) {
|
||||
validDataSources.push(dataSource);
|
||||
}
|
||||
});
|
||||
|
||||
const DATASOURCES_OPTIONS = validDataSources.map(dataSource => ({
|
||||
label: dataSource.name,
|
||||
value: dataSource.id,
|
||||
}));
|
||||
DATASOURCES_OPTIONS.push({
|
||||
label: "Create new Datasource",
|
||||
value: "createNew",
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{queryId ? (
|
||||
<QueryEditorForm
|
||||
isCreating={isCreating}
|
||||
location={this.props.location}
|
||||
applicationId={applicationId}
|
||||
pageId={pageId}
|
||||
allowSave={queryId in drafts}
|
||||
isSaving={isSaving[queryId]}
|
||||
isRunning={isRunning[queryId]}
|
||||
isDeleting={isDeleting[queryId]}
|
||||
onSubmit={this.handleSubmit}
|
||||
onSaveClick={this.handleSaveClick}
|
||||
onDeleteClick={this.handleDeleteClick}
|
||||
onRunClick={this.handleRunClick}
|
||||
dataSources={dataSources}
|
||||
createTemplate={createTemplate}
|
||||
DATASOURCES_OPTIONS={DATASOURCES_OPTIONS}
|
||||
selectedPluginPackage={selectedPluginPackage}
|
||||
executedQueryData={executedQueryData[queryId]}
|
||||
/>
|
||||
) : (
|
||||
<QueryHomeScreen
|
||||
dataSources={dataSources}
|
||||
applicationId={applicationId}
|
||||
pageId={pageId}
|
||||
history={this.props.history}
|
||||
location={this.props.location}
|
||||
isCreating={isCreating}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState): any => {
|
||||
const formData = getFormValues(QUERY_EDITOR_FORM_NAME)(state) as RestAction;
|
||||
const initialValues = getFormInitialValues(QUERY_EDITOR_FORM_NAME)(
|
||||
state,
|
||||
) as RestAction;
|
||||
const datasourceId = _.get(formData, "datasource.id");
|
||||
const selectedPluginPackage = getPluginPackageFromDatasourceId(
|
||||
state,
|
||||
datasourceId,
|
||||
);
|
||||
|
||||
return {
|
||||
apiPane: state.ui.apiPane,
|
||||
pluginIds: getPluginIdsOfPackageNames(state, PLUGIN_PACKAGE_DBS),
|
||||
dataSources: getDataSources(state),
|
||||
executedQueryData: state.ui.queryPane.runQuerySuccessData,
|
||||
queryPane: state.ui.queryPane,
|
||||
currentApplication: getCurrentApplication(state),
|
||||
formData: getFormValues(QUERY_EDITOR_FORM_NAME)(state) as RestAction,
|
||||
selectedPluginPackage,
|
||||
initialValues,
|
||||
isCreating: state.ui.queryPane.isCreating,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: any): any => ({
|
||||
submitForm: (name: string) => dispatch(submit(name)),
|
||||
updateAction: (data: RestAction) => dispatch(updateAction({ data })),
|
||||
deleteAction: (id: string) => dispatch(deleteQuery({ id })),
|
||||
runAction: (action: RestAction, actionId: string) =>
|
||||
dispatch(executeQuery({ action, actionId })),
|
||||
createTemplate: (template: any, name: string) => {
|
||||
dispatch(change(QUERY_EDITOR_FORM_NAME, name, template));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueryEditor);
|
||||
222
app/client/src/pages/Editor/QuerySidebar.tsx
Normal file
222
app/client/src/pages/Editor/QuerySidebar.tsx
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import styled from "styled-components";
|
||||
import { AppState } from "reducers";
|
||||
import { ActionDataState } from "reducers/entityReducers/actionsReducer";
|
||||
import { ApiPaneReduxState } from "reducers/uiReducers/apiPaneReducer";
|
||||
import EditorSidebar from "pages/Editor/EditorSidebar";
|
||||
import { QUERY_CONSTANT } from "constants/QueryEditorConstants";
|
||||
import { QueryEditorRouteParams } from "constants/routes";
|
||||
import { Datasource } from "api/DatasourcesApi";
|
||||
import {
|
||||
createActionRequest,
|
||||
moveActionRequest,
|
||||
copyActionRequest,
|
||||
} from "actions/actionActions";
|
||||
import { deleteQuery } from "actions/queryPaneActions";
|
||||
import { RestAction } from "api/ActionAPI";
|
||||
import { changeQuery, initQueryPane } from "actions/queryPaneActions";
|
||||
import { getQueryActions } from "selectors/entitiesSelector";
|
||||
import { getNextEntityName } from "utils/AppsmithUtils";
|
||||
import { getDataSources } from "selectors/editorSelectors";
|
||||
import { QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID } from "constants/routes";
|
||||
|
||||
const ActionItem = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ActionName = styled.span`
|
||||
flex: 3;
|
||||
padding: 0 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
interface ReduxStateProps {
|
||||
queries: ActionDataState;
|
||||
apiPane: ApiPaneReduxState;
|
||||
actions: ActionDataState;
|
||||
dataSources: Datasource[];
|
||||
}
|
||||
|
||||
interface ReduxDispatchProps {
|
||||
createAction: (data: Partial<RestAction>) => void;
|
||||
onQueryChange: (id: string, pluginType: string) => void;
|
||||
initQueryPane: (pluginType: string, urlId?: string) => void;
|
||||
moveAction: (
|
||||
id: string,
|
||||
pageId: string,
|
||||
name: string,
|
||||
originalPageId: string,
|
||||
) => void;
|
||||
copyAction: (id: string, pageId: string, name: string) => void;
|
||||
deleteAction: (id: string) => void;
|
||||
}
|
||||
|
||||
type Props = ReduxStateProps &
|
||||
ReduxDispatchProps &
|
||||
RouteComponentProps<QueryEditorRouteParams>;
|
||||
|
||||
class QuerySidebar extends React.Component<Props> {
|
||||
componentDidMount(): void {
|
||||
this.props.initQueryPane(QUERY_CONSTANT, this.props.match.params.queryId);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Readonly<Props>): boolean {
|
||||
if (
|
||||
Object.keys(nextProps.apiPane.drafts) !==
|
||||
Object.keys(this.props.apiPane.drafts)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return nextProps.actions !== this.props.actions;
|
||||
}
|
||||
|
||||
handleCreateNew = () => {
|
||||
const { actions } = this.props;
|
||||
const { pageId } = this.props.match.params;
|
||||
const pageApiNames = actions
|
||||
.filter(a => a.config.pageId === pageId)
|
||||
.map(a => a.config.name);
|
||||
const newName = getNextEntityName("Query", pageApiNames);
|
||||
this.props.createAction({ name: newName, pageId });
|
||||
};
|
||||
|
||||
handleCreateNewQuery = (dataSourceId: string, pageId: string) => {
|
||||
const { actions } = this.props;
|
||||
const pageApiNames = actions
|
||||
.filter(a => a.config.pageId === pageId)
|
||||
.map(a => a.config.name);
|
||||
const newQueryName = getNextEntityName("Query", pageApiNames);
|
||||
this.props.createAction({
|
||||
name: newQueryName,
|
||||
pageId,
|
||||
datasource: {
|
||||
id: dataSourceId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
handleCreateNewQueryClick = (selectedPageId: string) => {
|
||||
const { history } = this.props;
|
||||
const { pageId, applicationId } = this.props.match.params;
|
||||
history.push(
|
||||
QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID(
|
||||
applicationId,
|
||||
pageId,
|
||||
selectedPageId,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
handleQueryChange = (queryId: string) => {
|
||||
this.props.onQueryChange(queryId, QUERY_CONSTANT);
|
||||
};
|
||||
|
||||
handleMove = (itemId: string, destinationPageId: string) => {
|
||||
const query = this.props.queries.filter(a => a.config.id === itemId)[0];
|
||||
const pageApiNames = this.props.queries
|
||||
.filter(a => a.config.pageId === destinationPageId)
|
||||
.map(a => a.config.name);
|
||||
let name = query.config.name;
|
||||
if (pageApiNames.indexOf(query.config.name) > -1) {
|
||||
name = getNextEntityName(name, pageApiNames);
|
||||
}
|
||||
this.props.moveAction(itemId, destinationPageId, name, query.config.pageId);
|
||||
};
|
||||
|
||||
handleCopy = (itemId: string, destinationPageId: string) => {
|
||||
const query = this.props.queries.filter(a => a.config.id === itemId)[0];
|
||||
const pageApiNames = this.props.queries
|
||||
.filter(a => a.config.pageId === destinationPageId)
|
||||
.map(a => a.config.name);
|
||||
let name = `${query.config.name}Copy`;
|
||||
if (pageApiNames.indexOf(name) > -1) {
|
||||
name = getNextEntityName(name, pageApiNames);
|
||||
}
|
||||
this.props.copyAction(itemId, destinationPageId, name);
|
||||
};
|
||||
|
||||
handleDelete = (itemId: string) => {
|
||||
this.props.deleteAction(itemId);
|
||||
};
|
||||
|
||||
renderItem = (query: RestAction) => {
|
||||
return (
|
||||
<ActionItem>
|
||||
<ActionName>{query.name}</ActionName>
|
||||
</ActionItem>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
apiPane: { drafts },
|
||||
apiPane: { isFetching },
|
||||
match: {
|
||||
params: { queryId },
|
||||
},
|
||||
queries,
|
||||
dataSources,
|
||||
} = this.props;
|
||||
const data = queries.map(a => a.config);
|
||||
|
||||
const validDataSources: Array<Datasource> = [];
|
||||
dataSources.forEach(dataSource => {
|
||||
if (dataSource.isValid) {
|
||||
validDataSources.push(dataSource);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<EditorSidebar
|
||||
isLoading={isFetching}
|
||||
list={data}
|
||||
selectedItemId={queryId}
|
||||
draftIds={Object.keys(drafts)}
|
||||
itemRender={this.renderItem}
|
||||
onItemCreateClick={this.handleCreateNewQueryClick}
|
||||
onItemSelected={this.handleQueryChange}
|
||||
moveItem={this.handleMove}
|
||||
copyItem={this.handleCopy}
|
||||
deleteItem={this.handleDelete}
|
||||
createButtonTitle="Create new query"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState): ReduxStateProps => ({
|
||||
queries: getQueryActions(state),
|
||||
apiPane: state.ui.apiPane,
|
||||
actions: state.entities.actions,
|
||||
dataSources: getDataSources(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Function): ReduxDispatchProps => ({
|
||||
createAction: (data: Partial<RestAction>) =>
|
||||
dispatch(createActionRequest(data)),
|
||||
onQueryChange: (queryId: string, pluginType: string) => {
|
||||
dispatch(changeQuery(queryId, pluginType));
|
||||
},
|
||||
initQueryPane: (pluginType: string, urlId?: string) =>
|
||||
dispatch(initQueryPane(pluginType, urlId)),
|
||||
moveAction: (
|
||||
id: string,
|
||||
destinationPageId: string,
|
||||
name: string,
|
||||
originalPageId: string,
|
||||
) =>
|
||||
dispatch(
|
||||
moveActionRequest({ id, destinationPageId, originalPageId, name }),
|
||||
),
|
||||
copyAction: (id: string, destinationPageId: string, name: string) =>
|
||||
dispatch(copyActionRequest({ id, destinationPageId, name })),
|
||||
deleteAction: (id: string) => dispatch(deleteQuery({ id })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QuerySidebar);
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import React from "react";
|
||||
import { Switch, withRouter, RouteComponentProps } from "react-router-dom";
|
||||
import ApiEditor from "./APIEditor";
|
||||
import QueryEditor from "./QueryEditor";
|
||||
import DataSourceEditor from "./DataSourceEditor";
|
||||
|
||||
import CurlImportForm from "./APIEditor/CurlImportForm";
|
||||
|
|
@ -8,6 +9,8 @@ import ProviderTemplates from "./APIEditor/ProviderTemplates";
|
|||
import {
|
||||
API_EDITOR_ID_URL,
|
||||
API_EDITOR_URL,
|
||||
QUERIES_EDITOR_URL,
|
||||
QUERIES_EDITOR_ID_URL,
|
||||
DATA_SOURCES_EDITOR_URL,
|
||||
DATA_SOURCES_EDITOR_ID_URL,
|
||||
BUILDER_PAGE_URL,
|
||||
|
|
@ -66,6 +69,9 @@ class EditorsRouter extends React.Component<
|
|||
) !== -1 ||
|
||||
this.props.location.pathname.indexOf(
|
||||
API_EDITOR_URL(applicationId, pageId),
|
||||
) !== -1 ||
|
||||
this.props.location.pathname.indexOf(
|
||||
QUERIES_EDITOR_URL(applicationId, pageId),
|
||||
) !== -1
|
||||
),
|
||||
};
|
||||
|
|
@ -87,6 +93,9 @@ class EditorsRouter extends React.Component<
|
|||
) !== -1 ||
|
||||
this.props.location.pathname.indexOf(
|
||||
API_EDITOR_URL(applicationId, pageId),
|
||||
) !== -1 ||
|
||||
this.props.location.pathname.indexOf(
|
||||
QUERIES_EDITOR_URL(applicationId, pageId),
|
||||
) !== -1
|
||||
),
|
||||
});
|
||||
|
|
@ -139,6 +148,19 @@ class EditorsRouter extends React.Component<
|
|||
component={ApiEditor}
|
||||
name={"ApiEditor"}
|
||||
/>
|
||||
<AppRoute
|
||||
exact
|
||||
path={QUERIES_EDITOR_URL()}
|
||||
component={QueryEditor}
|
||||
name={"QueryEditor"}
|
||||
/>
|
||||
<AppRoute
|
||||
exact
|
||||
path={QUERIES_EDITOR_ID_URL()}
|
||||
component={QueryEditor}
|
||||
name={"QueryEditor"}
|
||||
/>
|
||||
|
||||
<AppRoute
|
||||
exact
|
||||
path={getCurlImportPageURL()}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,10 @@ const actionsReducer = createReducer(initialState, {
|
|||
state: ActionDataState,
|
||||
action: ReduxAction<{ id: string }>,
|
||||
): ActionDataState => state.filter(a => a.config.id !== action.payload.id),
|
||||
[ReduxActionTypes.DELETE_QUERY_SUCCESS]: (
|
||||
state: ActionDataState,
|
||||
action: ReduxAction<{ id: string }>,
|
||||
): ActionDataState => state.filter(a => a.config.id !== action.payload.id),
|
||||
[ReduxActionTypes.EXECUTE_API_ACTION_REQUEST]: (
|
||||
state: ActionDataState,
|
||||
action: ReduxAction<{ id: string }>,
|
||||
|
|
@ -111,6 +115,7 @@ const actionsReducer = createReducer(initialState, {
|
|||
if (a.config.id === action.payload.id) {
|
||||
return { ...a, isLoading: false, data: action.payload.response };
|
||||
}
|
||||
|
||||
return a;
|
||||
});
|
||||
},
|
||||
|
|
@ -122,6 +127,7 @@ const actionsReducer = createReducer(initialState, {
|
|||
if (a.config.id === action.payload.actionId) {
|
||||
return { ...a, isLoading: false };
|
||||
}
|
||||
|
||||
return a;
|
||||
}),
|
||||
[ReduxActionTypes.RUN_API_REQUEST]: (
|
||||
|
|
@ -135,6 +141,7 @@ const actionsReducer = createReducer(initialState, {
|
|||
isLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
return a;
|
||||
}),
|
||||
[ReduxActionTypes.RUN_API_SUCCESS]: (
|
||||
|
|
@ -149,6 +156,19 @@ const actionsReducer = createReducer(initialState, {
|
|||
return a;
|
||||
});
|
||||
},
|
||||
[ReduxActionTypes.RUN_QUERY_SUCCESS]: (
|
||||
state: ActionDataState,
|
||||
action: ReduxAction<{ actionId: string; data: ActionResponse }>,
|
||||
): ActionDataState => {
|
||||
const actionId: string = action.payload.actionId;
|
||||
|
||||
return state.map(a => {
|
||||
if (a.config.id === actionId) {
|
||||
return { ...a, isLoading: false, data: action.payload.data };
|
||||
}
|
||||
return a;
|
||||
});
|
||||
},
|
||||
[ReduxActionErrorTypes.RUN_API_ERROR]: (
|
||||
state: ActionDataState,
|
||||
action: ReduxAction<{ id: string }>,
|
||||
|
|
@ -157,6 +177,7 @@ const actionsReducer = createReducer(initialState, {
|
|||
if (a.config.id === action.payload.id) {
|
||||
return { ...a, isLoading: false };
|
||||
}
|
||||
|
||||
return a;
|
||||
}),
|
||||
[ReduxActionTypes.MOVE_ACTION_INIT]: (
|
||||
|
|
@ -178,6 +199,7 @@ const actionsReducer = createReducer(initialState, {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
return a;
|
||||
}),
|
||||
[ReduxActionTypes.MOVE_ACTION_SUCCESS]: (
|
||||
|
|
@ -188,6 +210,7 @@ const actionsReducer = createReducer(initialState, {
|
|||
if (a.config.id === action.payload.id) {
|
||||
return { ...a, config: action.payload };
|
||||
}
|
||||
|
||||
return a;
|
||||
}),
|
||||
[ReduxActionErrorTypes.MOVE_ACTION_ERROR]: (
|
||||
|
|
@ -204,6 +227,7 @@ const actionsReducer = createReducer(initialState, {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
return a;
|
||||
}),
|
||||
[ReduxActionTypes.COPY_ACTION_INIT]: (
|
||||
|
|
@ -240,6 +264,7 @@ const actionsReducer = createReducer(initialState, {
|
|||
config: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
return a;
|
||||
}),
|
||||
[ReduxActionErrorTypes.COPY_ACTION_ERROR]: (
|
||||
|
|
@ -257,6 +282,7 @@ const actionsReducer = createReducer(initialState, {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { DatasourcePaneReduxState } from "./uiReducers/datasourcePaneReducer";
|
|||
import { ApplicationsReduxState } from "./uiReducers/applicationsReducer";
|
||||
import { PageListReduxState } from "./entityReducers/pageListReducer";
|
||||
import { ApiPaneReduxState } from "./uiReducers/apiPaneReducer";
|
||||
import { QueryPaneReduxState } from "./uiReducers/queryPaneReducer";
|
||||
import { PluginDataState } from "reducers/entityReducers/pluginsReducer";
|
||||
import { AuthState } from "reducers/uiReducers/authReducer";
|
||||
import { OrgReduxState } from "reducers/uiReducers/orgReducer";
|
||||
|
|
@ -51,6 +52,7 @@ export interface AppState {
|
|||
importedCollections: ImportedCollectionsReduxState;
|
||||
providers: ProvidersReduxState;
|
||||
imports: ImportReduxState;
|
||||
queryPane: QueryPaneReduxState;
|
||||
datasourcePane: DatasourcePaneReduxState;
|
||||
};
|
||||
entities: {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { widgetDraggingReducer } from "./dragResizeReducer";
|
|||
import importedCollectionsReducer from "./importedCollectionsReducer";
|
||||
import providersReducer from "./providerReducer";
|
||||
import importReducer from "./importReducer";
|
||||
import queryPaneReducer from "./queryPaneReducer";
|
||||
|
||||
const uiReducer = combineReducers({
|
||||
widgetSidebar: widgetSidebarReducer,
|
||||
|
|
@ -30,6 +31,7 @@ const uiReducer = combineReducers({
|
|||
importedCollections: importedCollectionsReducer,
|
||||
providers: providersReducer,
|
||||
imports: importReducer,
|
||||
queryPane: queryPaneReducer,
|
||||
datasourcePane: datasourcePaneReducer,
|
||||
});
|
||||
export default uiReducer;
|
||||
|
|
|
|||
163
app/client/src/reducers/uiReducers/queryPaneReducer.ts
Normal file
163
app/client/src/reducers/uiReducers/queryPaneReducer.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { createReducer } from "utils/AppsmithUtils";
|
||||
import {
|
||||
ReduxActionTypes,
|
||||
ReduxActionErrorTypes,
|
||||
ReduxAction,
|
||||
} from "constants/ReduxActionConstants";
|
||||
import { RestAction } from "api/ActionAPI";
|
||||
|
||||
const initialState: QueryPaneReduxState = {
|
||||
isFetching: false,
|
||||
isCreating: false,
|
||||
isRunning: {},
|
||||
isSaving: {},
|
||||
isDeleting: {},
|
||||
runQuerySuccessData: {},
|
||||
lastUsed: "",
|
||||
};
|
||||
|
||||
export interface QueryPaneReduxState {
|
||||
isFetching: boolean;
|
||||
isRunning: Record<string, boolean>;
|
||||
isSaving: Record<string, boolean>;
|
||||
isDeleting: Record<string, boolean>;
|
||||
runQuerySuccessData: {};
|
||||
lastUsed: string;
|
||||
isCreating: boolean;
|
||||
}
|
||||
|
||||
const queryPaneReducer = createReducer(initialState, {
|
||||
[ReduxActionTypes.CREATE_ACTION_INIT]: (state: QueryPaneReduxState) => {
|
||||
return {
|
||||
...state,
|
||||
isCreating: true,
|
||||
};
|
||||
},
|
||||
[ReduxActionTypes.CREATE_ACTION_SUCCESS]: (state: QueryPaneReduxState) => {
|
||||
return {
|
||||
...state,
|
||||
isCreating: false,
|
||||
};
|
||||
},
|
||||
[ReduxActionTypes.CREATE_ACTION_ERROR]: (state: QueryPaneReduxState) => {
|
||||
return {
|
||||
...state,
|
||||
isCreating: false,
|
||||
};
|
||||
},
|
||||
[ReduxActionTypes.QUERY_PANE_CHANGE]: (
|
||||
state: QueryPaneReduxState,
|
||||
action: ReduxAction<{ id: string }>,
|
||||
) => ({
|
||||
...state,
|
||||
lastUsed: action.payload.id,
|
||||
}),
|
||||
[ReduxActionTypes.UPDATE_ACTION_INIT]: (
|
||||
state: QueryPaneReduxState,
|
||||
action: ReduxAction<{ data: RestAction }>,
|
||||
) => ({
|
||||
...state,
|
||||
isSaving: {
|
||||
...state.isSaving,
|
||||
[action.payload.data.id]: true,
|
||||
},
|
||||
}),
|
||||
[ReduxActionTypes.UPDATE_ACTION_SUCCESS]: (
|
||||
state: QueryPaneReduxState,
|
||||
action: ReduxAction<{ data: RestAction }>,
|
||||
) => ({
|
||||
...state,
|
||||
isSaving: {
|
||||
...state.isSaving,
|
||||
[action.payload.data.id]: false,
|
||||
},
|
||||
}),
|
||||
[ReduxActionErrorTypes.UPDATE_ACTION_ERROR]: (
|
||||
state: QueryPaneReduxState,
|
||||
action: ReduxAction<{ id: string }>,
|
||||
) => ({
|
||||
...state,
|
||||
isSaving: {
|
||||
...state.isSaving,
|
||||
[action.payload.id]: false,
|
||||
},
|
||||
}),
|
||||
[ReduxActionTypes.DELETE_QUERY_INIT]: (
|
||||
state: QueryPaneReduxState,
|
||||
action: ReduxAction<{ id: string }>,
|
||||
) => ({
|
||||
...state,
|
||||
isDeleting: {
|
||||
...state.isDeleting,
|
||||
[action.payload.id]: true,
|
||||
},
|
||||
}),
|
||||
[ReduxActionTypes.DELETE_QUERY_SUCCESS]: (
|
||||
state: QueryPaneReduxState,
|
||||
action: ReduxAction<{ id: string }>,
|
||||
) => ({
|
||||
...state,
|
||||
isDeleting: {
|
||||
...state.isDeleting,
|
||||
[action.payload.id]: false,
|
||||
},
|
||||
}),
|
||||
[ReduxActionErrorTypes.DELETE_QUERY_ERROR]: (
|
||||
state: QueryPaneReduxState,
|
||||
action: ReduxAction<{ id: string }>,
|
||||
) => ({
|
||||
...state,
|
||||
isDeleting: {
|
||||
...state.isDeleting,
|
||||
[action.payload.id]: false,
|
||||
},
|
||||
}),
|
||||
[ReduxActionTypes.EXECUTE_QUERY_REQUEST]: (
|
||||
state: any,
|
||||
action: ReduxAction<{ action: RestAction; actionId: string }>,
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
isRunning: {
|
||||
...state.isRunning,
|
||||
[action.payload.actionId]: true,
|
||||
},
|
||||
runQuerySuccessData: [],
|
||||
};
|
||||
},
|
||||
[ReduxActionTypes.CLEAR_PREVIOUSLY_EXECUTED_QUERY]: (state: any) => ({
|
||||
...state,
|
||||
runQuerySuccessData: [],
|
||||
}),
|
||||
|
||||
[ReduxActionTypes.RUN_QUERY_SUCCESS]: (
|
||||
state: any,
|
||||
action: ReduxAction<{ actionId: string; data: object }>,
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
isRunning: {
|
||||
...state.isRunning,
|
||||
[action.payload.actionId]: false,
|
||||
},
|
||||
runQuerySuccessData: {
|
||||
...state.runQuerySuccessData,
|
||||
[action.payload.actionId]: action.payload.data,
|
||||
},
|
||||
};
|
||||
},
|
||||
[ReduxActionErrorTypes.RUN_QUERY_ERROR]: (
|
||||
state: any,
|
||||
action: ReduxAction<{ actionId: string }>,
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
isRunning: {
|
||||
...state.isRunning,
|
||||
[action.payload.actionId]: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default queryPaneReducer;
|
||||
|
|
@ -73,6 +73,7 @@ import {
|
|||
import { ToastType } from "react-toastify";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import * as log from "loglevel";
|
||||
import { QUERY_CONSTANT } from "constants/QueryEditorConstants";
|
||||
|
||||
export const getAction = (
|
||||
state: AppState,
|
||||
|
|
@ -427,30 +428,42 @@ export function* updateActionSaga(
|
|||
actionPayload: ReduxAction<{ data: RestAction }>,
|
||||
) {
|
||||
try {
|
||||
const isApi = actionPayload.payload.data.pluginType !== "DB";
|
||||
const { data } = actionPayload.payload;
|
||||
const action = transformRestAction(data);
|
||||
let action = data;
|
||||
if (isApi) {
|
||||
action = transformRestAction(data);
|
||||
}
|
||||
const response: GenericApiResponse<RestAction> = yield ActionAPI.updateAPI(
|
||||
action,
|
||||
);
|
||||
const isValidResponse = yield validateResponse(response);
|
||||
if (isValidResponse) {
|
||||
AppToaster.show({
|
||||
message: `${actionPayload.payload.data.name} Action updated`,
|
||||
type: ToastType.SUCCESS,
|
||||
});
|
||||
|
||||
const pageName = yield select(
|
||||
getCurrentPageNameByActionId,
|
||||
response.data.id,
|
||||
);
|
||||
|
||||
if (action.pluginType === QUERY_CONSTANT) {
|
||||
AnalyticsUtil.logEvent("SAVE_QUERY", {
|
||||
queryName: action.name,
|
||||
pageName,
|
||||
});
|
||||
}
|
||||
AppToaster.show({
|
||||
message: `${actionPayload.payload.data.name} Action updated`,
|
||||
type: ToastType.SUCCESS,
|
||||
});
|
||||
|
||||
AnalyticsUtil.logEvent("SAVE_API", {
|
||||
apiId: response.data.id,
|
||||
apiName: response.data.name,
|
||||
pageName: pageName,
|
||||
});
|
||||
yield put(updateActionSuccess({ data: response.data }));
|
||||
yield put(runApiAction(data.id));
|
||||
if (actionPayload.payload.data.pluginType !== "DB") {
|
||||
yield put(runApiAction(data.id));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
yield put({
|
||||
|
|
|
|||
|
|
@ -361,13 +361,16 @@ function* formValueChangeSaga(
|
|||
}
|
||||
|
||||
function* handleActionCreatedSaga(actionPayload: ReduxAction<RestAction>) {
|
||||
const { id } = actionPayload.payload;
|
||||
const { id, pluginType } = actionPayload.payload;
|
||||
const action = yield select(getAction, id);
|
||||
const data = { ...action };
|
||||
yield put(initialize(API_EDITOR_FORM_NAME, data));
|
||||
const applicationId = yield select(getCurrentApplicationId);
|
||||
const pageId = yield select(getCurrentPageId);
|
||||
history.push(API_EDITOR_ID_URL(applicationId, pageId, id));
|
||||
|
||||
if (pluginType === "API") {
|
||||
yield put(initialize(API_EDITOR_FORM_NAME, data));
|
||||
const applicationId = yield select(getCurrentApplicationId);
|
||||
const pageId = yield select(getCurrentPageId);
|
||||
history.push(API_EDITOR_ID_URL(applicationId, pageId, id));
|
||||
}
|
||||
}
|
||||
|
||||
function* handleActionUpdatedSaga(
|
||||
|
|
@ -394,14 +397,18 @@ function* handleActionDeletedSaga(actionPayload: ReduxAction<{ id: string }>) {
|
|||
function* handleMoveOrCopySaga(actionPayload: ReduxAction<{ id: string }>) {
|
||||
const { id } = actionPayload.payload;
|
||||
const action = yield select(getAction, id);
|
||||
const { values }: { values: RestAction } = yield select(
|
||||
getFormData,
|
||||
API_EDITOR_FORM_NAME,
|
||||
);
|
||||
if (values.id === id) {
|
||||
yield put(initialize(API_EDITOR_FORM_NAME, action));
|
||||
} else {
|
||||
yield put(changeApi(id));
|
||||
const pluginType = action?.pluginType ?? "";
|
||||
|
||||
if (pluginType === "API") {
|
||||
const { values }: { values: RestAction } = yield select(
|
||||
getFormData,
|
||||
API_EDITOR_FORM_NAME,
|
||||
);
|
||||
if (values.id === id) {
|
||||
yield put(initialize(API_EDITOR_FORM_NAME, action));
|
||||
} else {
|
||||
yield put(changeApi(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { API_EDITOR_FORM_NAME } from "constants/forms";
|
|||
import { validateResponse } from "sagas/ErrorSagas";
|
||||
import CurlImportApi, { CurlImportRequest } from "api/ImportApi";
|
||||
import { ApiResponse } from "api/ApiResponses";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { AppToaster } from "components/editorComponents/ToastComponent";
|
||||
import { ToastType } from "react-toastify";
|
||||
import { CURL_IMPORT_SUCCESS } from "constants/messages";
|
||||
|
|
@ -19,6 +20,7 @@ import {
|
|||
getCurrentPageId,
|
||||
} from "selectors/editorSelectors";
|
||||
import { fetchActions } from "actions/actionActions";
|
||||
import { CURL } from "constants/ApiConstants";
|
||||
|
||||
export function* curlImportSaga(action: ReduxAction<CurlImportRequest>) {
|
||||
const { type, pageId, name } = action.payload;
|
||||
|
|
@ -36,6 +38,9 @@ export function* curlImportSaga(action: ReduxAction<CurlImportRequest>) {
|
|||
const currentPageId = yield select(getCurrentPageId);
|
||||
|
||||
if (isValidResponse) {
|
||||
AnalyticsUtil.logEvent("IMPORT_API", {
|
||||
importSource: CURL,
|
||||
});
|
||||
AppToaster.show({
|
||||
message: CURL_IMPORT_SUCCESS,
|
||||
type: ToastType.SUCCESS,
|
||||
|
|
|
|||
296
app/client/src/sagas/QueryPaneSagas.ts
Normal file
296
app/client/src/sagas/QueryPaneSagas.ts
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
import _, { take } from "lodash";
|
||||
import {
|
||||
all,
|
||||
select,
|
||||
put,
|
||||
takeEvery,
|
||||
call,
|
||||
takeLatest,
|
||||
} from "redux-saga/effects";
|
||||
import {
|
||||
ReduxAction,
|
||||
ReduxActionTypes,
|
||||
ReduxActionWithMeta,
|
||||
ReduxFormActionTypes,
|
||||
ReduxActionErrorTypes,
|
||||
} from "constants/ReduxActionConstants";
|
||||
import { getFormData } from "selectors/formSelectors";
|
||||
import { QUERY_EDITOR_FORM_NAME } from "constants/forms";
|
||||
import history from "utils/history";
|
||||
import {
|
||||
QUERIES_EDITOR_URL,
|
||||
QUERIES_EDITOR_ID_URL,
|
||||
APPLICATIONS_URL,
|
||||
} from "constants/routes";
|
||||
import {
|
||||
getCurrentApplicationId,
|
||||
getCurrentPageId,
|
||||
} from "selectors/editorSelectors";
|
||||
import { initialize } from "redux-form";
|
||||
import { getAction, getActionParams } from "./ActionSagas";
|
||||
import { AppState } from "reducers";
|
||||
import ActionAPI, {
|
||||
RestAction,
|
||||
PaginationField,
|
||||
ExecuteActionRequest,
|
||||
ActionApiResponse,
|
||||
} from "api/ActionAPI";
|
||||
import { QUERY_CONSTANT } from "constants/QueryEditorConstants";
|
||||
import { changeQuery, deleteQuerySuccess } from "actions/queryPaneActions";
|
||||
import { AppToaster } from "components/editorComponents/ToastComponent";
|
||||
import { ToastType } from "react-toastify";
|
||||
import { PageAction } from "constants/ActionConstants";
|
||||
import { transformRestAction } from "transformers/RestActionTransformer";
|
||||
import { isDynamicValue, getDynamicBindings } from "utils/DynamicBindingUtils";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { GenericApiResponse } from "api/ApiResponses";
|
||||
import { validateResponse } from "./ErrorSagas";
|
||||
import { getQueryName } from "selectors/entitiesSelector";
|
||||
|
||||
const getQueryDraft = (state: AppState, id: string) => {
|
||||
const drafts = state.ui.apiPane.drafts;
|
||||
if (id in drafts) return drafts[id];
|
||||
return {};
|
||||
};
|
||||
|
||||
const getActions = (state: AppState) =>
|
||||
state.entities.actions.map(a => a.config);
|
||||
|
||||
const getLastUsedAction = (state: AppState) => state.ui.queryPane.lastUsed;
|
||||
|
||||
function* initQueryPaneSaga(
|
||||
actionPayload: ReduxAction<{
|
||||
pluginType: string;
|
||||
id?: string;
|
||||
}>,
|
||||
) {
|
||||
let actions = yield select(getActions);
|
||||
while (!actions.length) {
|
||||
yield take(ReduxActionTypes.FETCH_ACTIONS_SUCCESS);
|
||||
actions = yield select(getActions);
|
||||
}
|
||||
const urlId = actionPayload.payload.id;
|
||||
const lastUsedId = yield select(getLastUsedAction);
|
||||
|
||||
let id = "";
|
||||
if (urlId) {
|
||||
id = urlId;
|
||||
} else if (lastUsedId) {
|
||||
id = lastUsedId;
|
||||
}
|
||||
|
||||
yield put(changeQuery(id, QUERY_CONSTANT));
|
||||
}
|
||||
|
||||
function* changeQuerySaga(
|
||||
actionPayload: ReduxAction<{ id: string; pluginType: string }>,
|
||||
) {
|
||||
const { id } = actionPayload.payload;
|
||||
// Typescript says Element does not have blur function but it does;
|
||||
document.activeElement &&
|
||||
"blur" in document.activeElement &&
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
document.activeElement.blur();
|
||||
|
||||
const applicationId = yield select(getCurrentApplicationId);
|
||||
const pageId = yield select(getCurrentPageId);
|
||||
if (!applicationId || !pageId) {
|
||||
history.push(APPLICATIONS_URL);
|
||||
return;
|
||||
}
|
||||
const action = yield select(getAction, id);
|
||||
if (!action) {
|
||||
history.push(QUERIES_EDITOR_URL(applicationId, pageId));
|
||||
return;
|
||||
}
|
||||
|
||||
const draft = yield select(getQueryDraft, id);
|
||||
const data = _.isEmpty(draft) ? action : draft;
|
||||
const URL = QUERIES_EDITOR_ID_URL(applicationId, pageId, id);
|
||||
yield put(initialize(QUERY_EDITOR_FORM_NAME, data));
|
||||
history.push(URL);
|
||||
}
|
||||
|
||||
function* updateDraftsSaga() {
|
||||
const { values } = yield select(getFormData, QUERY_EDITOR_FORM_NAME);
|
||||
if (!values.id) return;
|
||||
const action = yield select(getAction, values.id);
|
||||
if (_.isEqual(values, action)) {
|
||||
yield put({
|
||||
type: ReduxActionTypes.DELETE_API_DRAFT,
|
||||
payload: { id: values.id },
|
||||
});
|
||||
} else {
|
||||
yield put({
|
||||
type: ReduxActionTypes.UPDATE_API_DRAFT,
|
||||
payload: { id: values.id, draft: values },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function* formValueChangeSaga(
|
||||
actionPayload: ReduxActionWithMeta<string, { field: string; form: string }>,
|
||||
) {
|
||||
const { form } = actionPayload.meta;
|
||||
if (form !== QUERY_EDITOR_FORM_NAME) return;
|
||||
yield all([call(updateDraftsSaga)]);
|
||||
}
|
||||
|
||||
function* handleQueryCreatedSaga(actionPayload: ReduxAction<RestAction>) {
|
||||
const { id, pluginType } = actionPayload.payload;
|
||||
const action = yield select(getAction, id);
|
||||
const data = { ...action };
|
||||
if (pluginType === "DB") {
|
||||
yield put(initialize(QUERY_EDITOR_FORM_NAME, data));
|
||||
const applicationId = yield select(getCurrentApplicationId);
|
||||
const pageId = yield select(getCurrentPageId);
|
||||
history.replace(QUERIES_EDITOR_ID_URL(applicationId, pageId, id), {
|
||||
newQuery: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function* handleQueryDeletedSaga(actionPayload: ReduxAction<{ id: string }>) {
|
||||
const { id } = actionPayload.payload;
|
||||
const applicationId = yield select(getCurrentApplicationId);
|
||||
const pageId = yield select(getCurrentPageId);
|
||||
history.push(QUERIES_EDITOR_URL(applicationId, pageId));
|
||||
yield put({
|
||||
type: ReduxActionTypes.DELETE_API_DRAFT,
|
||||
payload: { id },
|
||||
});
|
||||
}
|
||||
|
||||
function* handleMoveOrCopySaga(actionPayload: ReduxAction<{ id: string }>) {
|
||||
const { id } = actionPayload.payload;
|
||||
const action = yield select(getAction, id);
|
||||
const pluginType = action?.pluginType ?? "";
|
||||
|
||||
if (pluginType === "DB") {
|
||||
const { values }: { values: RestAction } = yield select(
|
||||
getFormData,
|
||||
QUERY_EDITOR_FORM_NAME,
|
||||
);
|
||||
if (values.id === id) {
|
||||
yield put(initialize(QUERY_EDITOR_FORM_NAME, action));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* executeQuerySaga(
|
||||
actionPayload: ReduxAction<{
|
||||
action: RestAction;
|
||||
actionId: string;
|
||||
paginationField: PaginationField;
|
||||
}>,
|
||||
) {
|
||||
try {
|
||||
const {
|
||||
values,
|
||||
dirty,
|
||||
}: {
|
||||
values: RestAction;
|
||||
dirty: boolean;
|
||||
valid: boolean;
|
||||
} = yield select(getFormData, QUERY_EDITOR_FORM_NAME);
|
||||
const actionObject: PageAction = yield select(getAction, values.id);
|
||||
let action: ExecuteActionRequest["action"] = { id: values.id };
|
||||
let jsonPathKeys = actionObject.jsonPathKeys;
|
||||
|
||||
if (dirty) {
|
||||
action = _.omit(transformRestAction(values), "id") as RestAction;
|
||||
|
||||
const actionString = JSON.stringify(action);
|
||||
if (isDynamicValue(actionString)) {
|
||||
const { jsSnippets } = getDynamicBindings(actionString);
|
||||
// Replace cause the existing keys could have been updated
|
||||
jsonPathKeys = jsSnippets.filter(jsSnippet => !!jsSnippet);
|
||||
} else {
|
||||
jsonPathKeys = [];
|
||||
}
|
||||
}
|
||||
|
||||
const { paginationField } = actionPayload.payload;
|
||||
|
||||
const params = yield call(getActionParams, jsonPathKeys);
|
||||
const response: ActionApiResponse = yield ActionAPI.executeAction({
|
||||
action,
|
||||
params,
|
||||
paginationField,
|
||||
});
|
||||
|
||||
if (response.responseMeta && response.responseMeta.error) {
|
||||
throw response.responseMeta.error;
|
||||
}
|
||||
|
||||
yield put({
|
||||
type: ReduxActionTypes.RUN_QUERY_SUCCESS,
|
||||
payload: {
|
||||
data: response.data,
|
||||
actionId: actionPayload.payload.actionId,
|
||||
},
|
||||
});
|
||||
AppToaster.show({
|
||||
message: "Query ran successfully",
|
||||
type: ToastType.SUCCESS,
|
||||
});
|
||||
AnalyticsUtil.logEvent("RUN_QUERY", {
|
||||
queryName: actionPayload.payload.action.name,
|
||||
});
|
||||
} catch (error) {
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.RUN_QUERY_ERROR,
|
||||
payload: {
|
||||
actionId: actionPayload.payload.actionId,
|
||||
show: false,
|
||||
},
|
||||
});
|
||||
AppToaster.show({
|
||||
message: "Query is invalid. Please edit to make it valid",
|
||||
type: ToastType.ERROR,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function* deleteQuerySaga(actionPayload: ReduxAction<{ id: string }>) {
|
||||
try {
|
||||
const id = actionPayload.payload.id;
|
||||
const response: GenericApiResponse<RestAction> = yield ActionAPI.deleteAction(
|
||||
id,
|
||||
);
|
||||
const isValidResponse = yield validateResponse(response);
|
||||
if (isValidResponse) {
|
||||
const queryName = yield select(getQueryName, id);
|
||||
AnalyticsUtil.logEvent("DELETE_QUERY", {
|
||||
queryName,
|
||||
});
|
||||
AppToaster.show({
|
||||
message: `${response.data.name} Action deleted`,
|
||||
type: ToastType.SUCCESS,
|
||||
});
|
||||
yield put(deleteQuerySuccess({ id }));
|
||||
}
|
||||
} catch (error) {
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.DELETE_QUERY_ERROR,
|
||||
payload: { error, id: actionPayload.payload.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function* root() {
|
||||
yield all([
|
||||
takeEvery(ReduxActionTypes.CREATE_ACTION_SUCCESS, handleQueryCreatedSaga),
|
||||
takeLatest(ReduxActionTypes.DELETE_QUERY_INIT, deleteQuerySaga),
|
||||
takeEvery(ReduxActionTypes.DELETE_QUERY_SUCCESS, handleQueryDeletedSaga),
|
||||
takeEvery(ReduxActionTypes.MOVE_ACTION_SUCCESS, handleMoveOrCopySaga),
|
||||
takeEvery(ReduxActionTypes.COPY_ACTION_SUCCESS, handleMoveOrCopySaga),
|
||||
takeLatest(ReduxActionTypes.EXECUTE_QUERY_REQUEST, executeQuerySaga),
|
||||
takeEvery(ReduxActionTypes.QUERY_PANE_CHANGE, changeQuerySaga),
|
||||
takeEvery(ReduxActionTypes.INIT_QUERY_PANE, initQueryPaneSaga),
|
||||
// Intercepting the redux-form change actionType
|
||||
takeEvery(ReduxFormActionTypes.VALUE_CHANGE, formValueChangeSaga),
|
||||
takeEvery(ReduxFormActionTypes.ARRAY_REMOVE, formValueChangeSaga),
|
||||
]);
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import orgSagas from "./OrgSagas";
|
|||
import importedCollectionsSagas from "./CollectionSagas";
|
||||
import providersSagas from "./ProvidersSaga";
|
||||
import curlImportSagas from "./CurlImportSagas";
|
||||
import queryPaneSagas from "./QueryPaneSagas";
|
||||
import modalSagas from "./ModalSagas";
|
||||
import batchSagas from "./BatchSagas";
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ export function* rootSaga() {
|
|||
spawn(importedCollectionsSagas),
|
||||
spawn(providersSagas),
|
||||
spawn(curlImportSagas),
|
||||
spawn(queryPaneSagas),
|
||||
spawn(modalSagas),
|
||||
spawn(batchSagas),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -24,14 +24,14 @@ import * as log from "loglevel";
|
|||
const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig;
|
||||
const getWidgetSideBar = (state: AppState) => state.ui.widgetSidebar;
|
||||
const getPageListState = (state: AppState) => state.entities.pageList;
|
||||
export const getDataSources = (state: AppState) =>
|
||||
state.entities.datasources.list;
|
||||
export const getLastSelectedPage = (state: AppState) =>
|
||||
state.ui.apiPane.lastSelectedPage;
|
||||
|
||||
export const getProviderCategories = (state: AppState) =>
|
||||
state.ui.providers.providerCategories;
|
||||
|
||||
export const getDataSources = (state: AppState) =>
|
||||
state.entities.datasources.list;
|
||||
const getWidgets = (state: AppState): CanvasWidgetsReduxState =>
|
||||
state.entities.canvasWidgets;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { AppState } from "reducers";
|
||||
import { ActionDataState } from "reducers/entityReducers/actionsReducer";
|
||||
import {
|
||||
ActionDataState,
|
||||
ActionData,
|
||||
} from "reducers/entityReducers/actionsReducer";
|
||||
import { ActionResponse } from "api/ActionAPI";
|
||||
import { QUERY_CONSTANT } from "constants/QueryEditorConstants";
|
||||
import { API_CONSTANT } from "constants/ApiEditorConstants";
|
||||
import { createSelector } from "reselect";
|
||||
import { Page } from "constants/ReduxActionConstants";
|
||||
|
||||
|
|
@ -31,6 +36,19 @@ export const getPluginIdsOfNames = (
|
|||
return pluginIds;
|
||||
};
|
||||
|
||||
export const getPluginIdsOfPackageNames = (
|
||||
state: AppState,
|
||||
names: Array<string>,
|
||||
): Array<string> | undefined => {
|
||||
const plugins = state.entities.plugins.list.filter(plugin =>
|
||||
names.includes(plugin.packageName),
|
||||
);
|
||||
const pluginIds = plugins.map(plugin => plugin.id);
|
||||
|
||||
if (!pluginIds.length) return undefined;
|
||||
return pluginIds;
|
||||
};
|
||||
|
||||
export const getPluginNameFromDatasourceId = (
|
||||
state: AppState,
|
||||
datasourceId: string,
|
||||
|
|
@ -46,15 +64,6 @@ export const getPluginNameFromDatasourceId = (
|
|||
return plugin.name;
|
||||
};
|
||||
|
||||
export const getPluginNameFromId = (state: AppState, pluginId: string) => {
|
||||
const plugin = state.entities.plugins.list.find(
|
||||
plugin => plugin.id === pluginId,
|
||||
);
|
||||
|
||||
if (!plugin) return "";
|
||||
return plugin.name;
|
||||
};
|
||||
|
||||
export const getPluginPackageFromId = (state: AppState, pluginId: string) => {
|
||||
const plugin = state.entities.plugins.list.find(
|
||||
plugin => plugin.id === pluginId,
|
||||
|
|
@ -79,6 +88,15 @@ export const getPluginPackageFromDatasourceId = (
|
|||
return plugin.packageName;
|
||||
};
|
||||
|
||||
export const getPluginNameFromId = (state: AppState, pluginId: string) => {
|
||||
const plugin = state.entities.plugins.list.find(
|
||||
plugin => plugin.id === pluginId,
|
||||
);
|
||||
|
||||
if (!plugin) return "";
|
||||
return plugin.name;
|
||||
};
|
||||
|
||||
export const getPluginForm = (state: AppState, pluginId: string): [] => {
|
||||
return state.entities.plugins.formConfigs[pluginId];
|
||||
};
|
||||
|
|
@ -93,6 +111,34 @@ export const getDatasourceNames = (state: AppState): any =>
|
|||
state.entities.datasources.list.map(datasource => datasource.name);
|
||||
|
||||
export const getPlugins = (state: AppState) => state.entities.plugins.list;
|
||||
|
||||
export const getApiActions = (state: AppState): ActionDataState => {
|
||||
return state.entities.actions.filter((action: ActionData) => {
|
||||
return action.config.pluginType === API_CONSTANT;
|
||||
});
|
||||
};
|
||||
|
||||
export const getQueryName = (state: AppState, actionId: string): string => {
|
||||
const action = state.entities.actions.find((action: ActionData) => {
|
||||
return action.config.id === actionId;
|
||||
});
|
||||
|
||||
return action?.config.name ?? "";
|
||||
};
|
||||
|
||||
export const getPageName = (state: AppState, pageId: string): string => {
|
||||
const page = state.entities.pageList.pages.find((page: Page) => {
|
||||
return page.pageId === pageId;
|
||||
});
|
||||
|
||||
return page?.pageName ?? "";
|
||||
};
|
||||
|
||||
export const getQueryActions = (state: AppState): ActionDataState => {
|
||||
return state.entities.actions.filter((action: ActionData) => {
|
||||
return action.config.pluginType === QUERY_CONSTANT;
|
||||
});
|
||||
};
|
||||
const getCurrentPageId = (state: AppState) =>
|
||||
state.entities.pageList.currentPageId;
|
||||
|
||||
|
|
@ -112,5 +158,6 @@ export const getActionResponses = (
|
|||
state.entities.actions.forEach(a => {
|
||||
responses[a.config.id] = a.data;
|
||||
});
|
||||
|
||||
return responses;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ export type EventName =
|
|||
| "PREVIEW_APP"
|
||||
| "EDITOR_OPEN"
|
||||
| "CREATE_API"
|
||||
| "IMPORT_API"
|
||||
| "IMPORT_API_CLICK"
|
||||
| "SAVE_API"
|
||||
| "SAVE_API_CLICK"
|
||||
| "RUN_API"
|
||||
|
|
@ -33,7 +35,12 @@ export type EventName =
|
|||
| "DELETE_API"
|
||||
| "DELETE_API_CLICK"
|
||||
| "DUPLICATE_API"
|
||||
| "DUPLICATE_API_CLICK"
|
||||
| "RUN_QUERY"
|
||||
| "DELETE_QUERY"
|
||||
| "SAVE_QUERY"
|
||||
| "MOVE_API"
|
||||
| "MOVE_API_CLICK"
|
||||
| "API_SELECT"
|
||||
| "CREATE_API_CLICK"
|
||||
| "AUTO_COMPELTE_SHOW"
|
||||
|
|
@ -42,6 +49,7 @@ export type EventName =
|
|||
| "CREATE_APP"
|
||||
| "CREATE_DATA_SOURCE_CLICK"
|
||||
| "SAVE_DATA_SOURCE"
|
||||
| "CREATE_QUERY_CLICK"
|
||||
| "NAVIGATE"
|
||||
| "PAGE_LOAD"
|
||||
| "NAVIGATE_EDITOR"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { getAppsmithConfigs } from "configs";
|
|||
import * as Sentry from "@sentry/browser";
|
||||
import AnalyticsUtil from "./AnalyticsUtil";
|
||||
import FontFaceObserver from "fontfaceobserver";
|
||||
|
||||
import FormControlRegistry from "./FormControlRegistry";
|
||||
import { Property } from "api/ActionAPI";
|
||||
import _ from "lodash";
|
||||
|
|
@ -105,6 +104,17 @@ export const noop = () => {
|
|||
console.log("noop");
|
||||
};
|
||||
|
||||
export const createNewQueryName = (
|
||||
queries: ActionDataState,
|
||||
pageId: string,
|
||||
) => {
|
||||
const pageApiNames = queries
|
||||
.filter(a => a.config.pageId === pageId)
|
||||
.map(a => a.config.name);
|
||||
const newName = getNextEntityName("Query", pageApiNames);
|
||||
return newName;
|
||||
};
|
||||
|
||||
export const convertToString = (value: any): string => {
|
||||
if (_.isUndefined(value)) {
|
||||
return "";
|
||||
|
|
|
|||
1
app/client/typings/react-json-view/index.t.ts
Normal file
1
app/client/typings/react-json-view/index.t.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
declare module "react-json-view";
|
||||
Loading…
Reference in New Issue
Block a user