diff --git a/frontend/src/metabase/components/NewItemMenu/NewItemMenu.tsx b/frontend/src/metabase/components/NewItemMenu/NewItemMenu.tsx index 31aafc5bde4bc8c8c3c61305fc7e51af82bdd20a..dd1efe2d1b1831b11d7ac43514611a13a370f300 100644 --- a/frontend/src/metabase/components/NewItemMenu/NewItemMenu.tsx +++ b/frontend/src/metabase/components/NewItemMenu/NewItemMenu.tsx @@ -16,7 +16,6 @@ export interface NewItemMenuProps { triggerIcon?: string; triggerTooltip?: string; analyticsContext?: string; - isAdmin: boolean; hasDataAccess: boolean; hasNativeWrite: boolean; hasDatabaseWithJsonEngine: boolean; @@ -31,7 +30,6 @@ const NewItemMenu = ({ triggerIcon, triggerTooltip, analyticsContext, - isAdmin, hasDataAccess, hasNativeWrite, hasDatabaseWithJsonEngine, @@ -98,18 +96,9 @@ const NewItemMenu = ({ }, ); - if (isAdmin) { - items.push({ - title: t`HTTP Action`, - icon: "cloud", - link: "/action/create", - }); - } - return items; }, [ collectionId, - isAdmin, hasDataAccess, hasNativeWrite, hasDatabaseWithJsonEngine, diff --git a/frontend/src/metabase/containers/NewItemMenu/NewItemMenu.tsx b/frontend/src/metabase/containers/NewItemMenu/NewItemMenu.tsx index aaff2760cafae9fdb59ae469aaae03379d8fea85..62cabbcf75c86d29f1d19b4a6ac3c59a52f45688 100644 --- a/frontend/src/metabase/containers/NewItemMenu/NewItemMenu.tsx +++ b/frontend/src/metabase/containers/NewItemMenu/NewItemMenu.tsx @@ -3,7 +3,6 @@ import { connect } from "react-redux"; import { push } from "react-router-redux"; import { closeNavbar } from "metabase/redux/app"; import NewItemMenu from "metabase/components/NewItemMenu"; -import { getUserIsAdmin } from "metabase/selectors/user"; import { getHasDataAccess, getHasDatabaseWithJsonEngine, @@ -20,7 +19,6 @@ interface MenuOwnProps { } interface MenuStateProps { - isAdmin: boolean; hasDataAccess: boolean; hasNativeWrite: boolean; hasDatabaseWithJsonEngine: boolean; @@ -32,7 +30,6 @@ interface MenuDispatchProps { } const mapStateToProps = (state: State): MenuStateProps => ({ - isAdmin: getUserIsAdmin(state), hasDataAccess: getHasDataAccess(state), hasNativeWrite: getHasNativeWrite(state), hasDatabaseWithJsonEngine: getHasDatabaseWithJsonEngine(state), diff --git a/frontend/src/metabase/routes.jsx b/frontend/src/metabase/routes.jsx index e4dbba604174edc27a240e0a559278543b2d8308..1f738bbda9ba2d1fd9c3f4ad16c65af4de3bec2d 100644 --- a/frontend/src/metabase/routes.jsx +++ b/frontend/src/metabase/routes.jsx @@ -89,9 +89,6 @@ import SearchApp from "metabase/home/containers/SearchApp"; import { trackPageView } from "metabase/lib/analytics"; import { getAdminPaths } from "metabase/admin/app/selectors"; -import CreateActionPage from "metabase/writeback/containers/CreateActionPage"; -import EditActionPage from "metabase/writeback/containers/EditActionPage"; - const MetabaseIsSetup = UserAuthWrapper({ predicate: authData => authData.hasUserSetup, failureRedirectPath: "/setup", @@ -356,11 +353,6 @@ export const getRoutes = store => ( {/* ADMIN */} {getAdminRoutes(store, CanAccessSettings, IsAdmin)} - - <Route path="/action"> - <Route path="create" component={CreateActionPage} /> - <Route path=":actionId" component={EditActionPage} /> - </Route> </Route> </Route> diff --git a/frontend/src/metabase/writeback/actions.ts b/frontend/src/metabase/writeback/actions.ts index 2d8d99e0c706b26637dc828a1fa7c53cb556b08e..6f93ecf74725c31dd7f7307ba404881459bf1c8e 100644 --- a/frontend/src/metabase/writeback/actions.ts +++ b/frontend/src/metabase/writeback/actions.ts @@ -1,25 +1,7 @@ -import { t } from "ttag"; -import { push } from "react-router-redux"; - -import Actions from "metabase/entities/actions"; import { ActionsApi } from "metabase/services"; -import { addUndo } from "metabase/redux/undo"; import Table from "metabase-lib/lib/metadata/Table"; -import { - Parameter, - ParameterId, - ParameterTarget, -} from "metabase-types/types/Parameter"; -import { Dispatch, GetState } from "metabase-types/store"; - -import { - HttpActionErrorHandle, - HttpActionResponseHandle, - HttpActionTemplate, -} from "./types"; - export type InsertRowPayload = { table: Table; values: Record<string, unknown>; @@ -116,61 +98,3 @@ export const deleteManyRows = (payload: BulkDeletePayload) => { { bodyParamName: "body" }, ); }; - -export type CreateHttpActionPayload = { - name: string; - description: string; - template: HttpActionTemplate; - response_handle: HttpActionResponseHandle; - error_handle: HttpActionErrorHandle; - parameters: Record<ParameterId, Parameter>; - parameter_mappings: Record<ParameterId, ParameterTarget>; -}; - -export const createHttpAction = - (payload: CreateHttpActionPayload) => - async (dispatch: Dispatch, getState: GetState) => { - const { - name, - description, - template, - error_handle = null, - response_handle = null, - parameters, - parameter_mappings, - } = payload; - const data = { - method: template.method || "GET", - url: template.url, - body: template.body || JSON.stringify({}), - headers: JSON.stringify(template.headers || {}), - parameters: template.parameters || {}, - parameter_mappings: template.parameter_mappings || {}, - }; - const newAction = { - name, - type: "http", - description, - template: data, - error_handle, - response_handle, - parameters, - parameter_mappings, - }; - const response = await dispatch(Actions.actions.create(newAction)); - const action = Actions.HACK_getObjectFromAction(response); - if (action.id) { - dispatch( - addUndo({ - message: t`Action saved!`, - }), - ); - dispatch(push(`/action/${action.id}`)); - } else { - dispatch( - addUndo({ - message: t`Could not save action`, - }), - ); - } - }; diff --git a/frontend/src/metabase/writeback/components/HttpAction/BodyTab.tsx b/frontend/src/metabase/writeback/components/HttpAction/BodyTab.tsx deleted file mode 100644 index 8e570399c4d7ec96e71772fba0dca9209afdfa17..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/BodyTab.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from "react"; -import _ from "underscore"; - -import JsonEditor from "./JsonEditor/JsonEditor"; - -type Props = { - contentType: string; - setContentType: (contentType: string) => void; - - body: string; - setBody: (contentType: string) => void; -}; - -const BodyTab: React.FC<Props> = (props: Props) => { - if (props.contentType === "application/json") { - return <Json {...props} />; - } - - return null; -}; - -const Json: React.FC<Props> = ({ body, setBody }: Props) => { - return <JsonEditor value={body} onChange={setBody} />; -}; - -export default BodyTab; diff --git a/frontend/src/metabase/writeback/components/HttpAction/CompactSelect.tsx b/frontend/src/metabase/writeback/components/HttpAction/CompactSelect.tsx deleted file mode 100644 index fe602415e2dfa0e26cf844113293704d90076cda..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/CompactSelect.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import styled from "@emotion/styled"; - -import Select from "metabase/core/components/Select"; -import SelectButton from "metabase/core/components/SelectButton"; - -import { color } from "metabase/lib/colors"; - -const CompactSelect = styled(Select)` - ${SelectButton.Root} { - border: none; - border-radius: 6px; - - min-width: 80px; - - color: ${color("text-medium")}; - } - - ${SelectButton.Content} { - margin-right: 6px; - } - - ${SelectButton.Icon} { - margin-left: 0; - } - - &:hover { - ${SelectButton.Root} { - background-color: ${color("bg-light")}; - } - } -`; - -CompactSelect.defaultProps = { - width: 120, -}; - -export default CompactSelect; diff --git a/frontend/src/metabase/writeback/components/HttpAction/Header.styled.tsx b/frontend/src/metabase/writeback/components/HttpAction/Header.styled.tsx deleted file mode 100644 index ca151be964252edf74bbfef30413e29745d636e7..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/Header.styled.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import styled from "@emotion/styled"; -import EditableTextBase from "metabase/core/components/EditableText"; -import ButtonBase from "metabase/core/components/Button"; -import { color, alpha, lighten } from "metabase/lib/colors"; -import { space } from "metabase/styled-components/theme"; - -export const Container = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - background-color: ${color("white")}; - padding: ${space(1)} ${space(3)}; -`; - -export const LeftHeader = styled.div` - display: flex; - align-items: center; - color: ${color("text-light")}; - - & > * ~ * { - margin-left: ${space(2)}; - margin-right: ${space(2)}; - } -`; - -export const RightHeader = styled(ButtonBase)<{ enabled: boolean }>` - font-weight: 600; - color: ${props => (props.enabled ? color("brand") : color("text-medium"))}; - background-opacity: 0.25; - padding: 0; - - &:hover { - color: ${color("accent0-light")}; - } -`; - -export const EditableText = styled(EditableTextBase)` - font-weight: bold; - font-size: 0.85em; -`; - -export const Option = styled.div` - color: ${color("text-light")}; -`; diff --git a/frontend/src/metabase/writeback/components/HttpAction/Header.tsx b/frontend/src/metabase/writeback/components/HttpAction/Header.tsx deleted file mode 100644 index 44fd12a6039902d53b7ed05c4dc3d09c27234b08..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/Header.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from "react"; -import { t } from "ttag"; - -import { ActionType } from "metabase/writeback/types"; - -import CompactSelect from "./CompactSelect"; - -import { - Container, - LeftHeader, - EditableText, - Option, - RightHeader, -} from "./Header.styled"; - -type Props = { - name: string; - onNameChange: (name: string) => void; - - type: ActionType; - setType?: (type: ActionType) => void; - - onCommit: () => void; - canSave: boolean; -}; - -const Header: React.FC<Props> = ({ - name, - onNameChange, - type, - setType, - canSave, - onCommit, -}) => { - const OPTS = [ - { value: "http", name: "HTTP" }, - // Not supported yet - { value: "query", name: t`Query`, disabled: true }, - ]; - - return ( - <Container> - <LeftHeader> - <EditableText initialValue={name} onChange={onNameChange} /> - {setType ? ( - <CompactSelect - className="text-light" - options={OPTS} - value={type} - onChange={(value: ActionType) => setType(value)} - /> - ) : ( - <Option className="text-light"> - {OPTS.find(({ value }) => value === type)?.name} - </Option> - )} - </LeftHeader> - <RightHeader - borderless - enabled={canSave} - disabled={!canSave} - onClick={canSave ? onCommit : undefined} - > - {t`Save`} - </RightHeader> - </Container> - ); -}; - -export default Header; diff --git a/frontend/src/metabase/writeback/components/HttpAction/HttpAction.styled.tsx b/frontend/src/metabase/writeback/components/HttpAction/HttpAction.styled.tsx deleted file mode 100644 index 685650cb82174400ce933df9d21aaaa896e14d1e..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/HttpAction.styled.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import styled from "@emotion/styled"; -import EditableTextBase from "metabase/core/components/EditableText"; -import { color, alpha, lighten } from "metabase/lib/colors"; -import { space } from "metabase/styled-components/theme"; - -export const Grid = styled.div` - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - width: 100%; - height: 100%; -`; - -export const Tab = styled.div` - flex-grow: 1; -`; - -export const PersistentTab = styled.div<{ active: boolean }>` - flex-grow: 1; - display: ${props => (props.active ? "block" : "none")}; - padding: 1rem; -`; - -const BORDER = `1px solid ${color("border")}`; - -export const LeftColumn = styled.div` - display: flex; - flex-direction: column; - - border-top: ${BORDER}; - border-right: ${BORDER}; - - background-color: ${color("content")}; -`; - -export const LeftTabs = styled.div` - border-right: ${BORDER}; -`; - -export const RightColumn = styled.div` - display: flex; - flex-direction: column; - border-top: ${BORDER}; -`; - -export const RightTabs = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - border-bottom: ${BORDER}; - padding: 0 ${space(2)}; -`; - -export const MethodContainer = styled.div` - border-bottom: ${BORDER}; - padding: ${space(1)} ${space(3)}; -`; - -export const UrlContainer = styled.div` - padding: ${space(2)} 0; - border-bottom: ${BORDER}; -`; - -export const BodyContainer = styled.div` - display: flex; - flex-direction: column; - flex-grow: 1; - background-color: ${color("white")}; - border-bottom: ${BORDER}; -`; - -export const Description = styled.div` - background-color: ${color("white")}; - padding: ${space(2)} ${space(2)} ${space(2)} ${space(3)}; - border-bottom: ${BORDER}; -`; - -export const EditableText = styled(EditableTextBase)` - color: ${color("text-light")}; -`; diff --git a/frontend/src/metabase/writeback/components/HttpAction/HttpAction.tsx b/frontend/src/metabase/writeback/components/HttpAction/HttpAction.tsx deleted file mode 100644 index 59565406add110b23f2b0af83ffabf8035a811a2..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/HttpAction.tsx +++ /dev/null @@ -1,265 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from "react"; -import { t } from "ttag"; - -import MethodSelector from "./MethodSelector"; -import Tabs from "./Tabs"; -import HttpHeaderTab, { Headers } from "./HttpHeaderTab"; -import BodyTab from "./BodyTab"; -import UrlInput from "./UrlInput"; -import CompactSelect from "./CompactSelect"; -import ParametersTab from "./ParametersTab"; -import { TemplateTags } from "metabase-types/types/Query"; - -import { - BodyContainer, - Tab, - PersistentTab, - EditableText, - Description, - Grid, - LeftColumn, - LeftTabs, - MethodContainer, - RightColumn, - RightTabs, - UrlContainer, -} from "./HttpAction.styled"; -import ResponseTab from "./ResponseTab"; - -type Props = { - description: string; - onDescriptionChange: (description: string) => void; - - data: any; - onDataChange: (data: any) => void; - - templateTags: TemplateTags; - onTemplateTagsChange: (templateTags: TemplateTags) => void; - - responseHandler: string; - onResponseHandlerChange: (responseHandler: string) => void; - - errorHandler: string; - onErrorHandlerChange: (errorHandler: string) => void; -}; - -const HttpAction: React.FC<Props> = ({ - onDataChange, - data, - templateTags, - description, - onDescriptionChange, - onTemplateTagsChange, - responseHandler, - onResponseHandlerChange, - errorHandler, - onErrorHandlerChange, -}) => { - const { protocol, url, method, initialHeaders, body } = React.useMemo(() => { - const [protocol, url] = (data.url || "https://").split("://", 2); - const initialHeaders: Headers = Object.entries( - (typeof data.headers === "string" - ? JSON.parse(data.headers) - : data.headers) || {}, - ).map(([key, value]) => ({ key, value: value as string })); - return { - protocol, - url, - method: data.method || "GET", - initialHeaders, - body: data.body, - }; - }, [data]); - const [headers, setHeaders] = React.useState<Headers>(initialHeaders); - - return ( - <HttpActionInner - description={description} - onDescriptionChange={onDescriptionChange} - templateTags={templateTags} - onTemplateTagsChange={onTemplateTagsChange} - method={method} - setMethod={value => { - onDataChange({ method: value }); - }} - url={url} - setUrl={value => { - onDataChange({ url: `${protocol}://${value}` }); - }} - protocol={protocol} - setProtocol={value => { - onDataChange({ url: `${value}://${url}` }); - }} - body={body} - setBody={value => { - onDataChange({ body: value }); - }} - headers={headers} - setHeaders={value => { - setHeaders(value); - onDataChange({ - headers: Object.fromEntries( - value.map(({ key, value }) => [key, value]), - ), - }); - }} - responseHandler={responseHandler} - onResponseHandlerChange={onResponseHandlerChange} - errorHandler={errorHandler} - onErrorHandlerChange={onErrorHandlerChange} - /> - ); -}; - -type InnerProps = { - method: string; - setMethod: (newValue: string) => void; - - url: string; - setUrl: (newValue: string) => void; - - protocol: string; - setProtocol: (newValue: string) => void; - - body: string; - setBody: (newValue: string) => void; - - headers: Headers; - setHeaders: (newValue: Headers) => void; - - description: string; - onDescriptionChange: (description: string) => void; - - responseHandler: string; - onResponseHandlerChange: (errorHandler: string) => void; - - errorHandler: string; - onErrorHandlerChange: (errorHandler: string) => void; - - templateTags: TemplateTags; - onTemplateTagsChange: (templateTags: TemplateTags) => void; -}; - -const HttpActionInner: React.FC<InnerProps> = ({ - method, - setMethod, - url, - setUrl, - protocol, - setProtocol, - body, - setBody, - headers, - setHeaders, - templateTags, - description, - onDescriptionChange, - onTemplateTagsChange, - responseHandler, - onResponseHandlerChange, - errorHandler, - onErrorHandlerChange, -}) => { - const [currentParamTab, setCurrentParamTab] = React.useState( - PARAM_TABS[0].name, - ); - const [currentConfigTab, setCurrentConfigTab] = React.useState( - CONFIG_TABS[0].name, - ); - const [contentType, setContentType] = React.useState("application/json"); - return ( - <Grid> - <LeftColumn> - <MethodContainer> - <MethodSelector value={method} setValue={setMethod} /> - </MethodContainer> - <UrlContainer> - <UrlInput - url={url} - setUrl={setUrl} - protocol={protocol} - setProtocol={setProtocol} - /> - </UrlContainer> - <Description> - <EditableText - className="text-sm text-light" - placeholder={t`Enter an action description...`} - initialValue={description} - onChange={onDescriptionChange} - /> - </Description> - <BodyContainer> - <LeftTabs> - <Tabs - tabs={PARAM_TABS} - currentTab={currentParamTab} - setCurrentTab={setCurrentParamTab} - /> - </LeftTabs> - <Tab> - <ParametersTab - templateTags={templateTags} - onTemplateTagsChange={onTemplateTagsChange} - /> - </Tab> - </BodyContainer> - </LeftColumn> - <RightColumn> - <RightTabs> - <div> - <Tabs - tabs={CONFIG_TABS} - currentTab={currentConfigTab} - setCurrentTab={setCurrentConfigTab} - /> - </div> - <div> - <CompactSelect - options={CONTENT_TYPE} - value={contentType} - onChange={(value: string) => setContentType(value)} - /> - </div> - </RightTabs> - <PersistentTab active={currentConfigTab === "body"}> - <BodyTab - contentType={contentType} - setContentType={setContentType} - body={body} - setBody={setBody} - /> - </PersistentTab> - <PersistentTab active={currentConfigTab === "headers"}> - <HttpHeaderTab headers={headers} setHeaders={setHeaders} /> - </PersistentTab> - <PersistentTab active={currentConfigTab === "response"}> - <ResponseTab - responseHandler={responseHandler} - onResponseHandlerChange={onResponseHandlerChange} - errorHandler={errorHandler} - onErrorHandlerChange={onErrorHandlerChange} - /> - </PersistentTab> - </RightColumn> - </Grid> - ); -}; - -const CONFIG_TABS = [ - { name: "body", label: t`Body` }, - { name: "headers", label: t`Headers` }, - { name: "response", label: t`Response` }, -]; - -const PARAM_TABS = [{ name: "params", label: t`Parameters` }]; - -const CONTENT_TYPE = [ - { - value: "application/json", - name: "JSON", - }, -]; - -export default HttpAction; diff --git a/frontend/src/metabase/writeback/components/HttpAction/HttpHeaderTab.styled.tsx b/frontend/src/metabase/writeback/components/HttpAction/HttpHeaderTab.styled.tsx deleted file mode 100644 index 5e6e26bf0ad825e47a7eaa07155293be66fbc7b2..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/HttpHeaderTab.styled.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import styled from "@emotion/styled"; -import ButtonBase from "metabase/core/components/Button"; -import { color } from "metabase/lib/colors"; -import { space } from "metabase/styled-components/theme"; - -export const Grid = styled.div` - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - grid-gap: 0.5rem; -`; - -export const Input = styled.input` - display: flex; - height: 100%; - width: 100%; - border: none; - background-color: ${color("bg-medium")}; - padding: ${space(1)} ${space(1)}; -`; - -export const HeaderRow = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - - width: 100%; -`; - -export const DeleteButton = styled(ButtonBase)` - font-weight: bold; - color: ${color("brand")}; - background-opacity: 0.25; - - &:hover { - background-color: ${color("accent0-light")}; - background-opacity: 0.25; - } -`; - -export const AddButton = styled(ButtonBase)` - font-weight: bold; - color: ${color("brand")}; - background-opacity: 0.25; - - &:hover { - background-color: ${color("accent0-light")}; - background-opacity: 0.25; - } -`; - -export const TitleRowContainer = styled.div` - display: flex; - align-items: center; - justify-content: space-between; -`; - -export const LeftHeader = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - height: 100%; -`; - -export const RightHeader = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - height: 100%; - width: 100%; -`; - -export const Title = styled.span` - display: block; - font-weight: 600; -`; diff --git a/frontend/src/metabase/writeback/components/HttpAction/HttpHeaderTab.tsx b/frontend/src/metabase/writeback/components/HttpAction/HttpHeaderTab.tsx deleted file mode 100644 index 7cbcdd258e8bd8675175a29fd965d28ecad9e3e3..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/HttpHeaderTab.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from "react"; -import { t } from "ttag"; -import { assoc } from "icepick"; - -import { - Input, - Grid, - LeftHeader, - RightHeader, - HeaderRow, - DeleteButton, - AddButton, - Title, - TitleRowContainer, -} from "./HttpHeaderTab.styled"; - -export type Headers = { - key: string; - value: string; -}[]; - -type Props = { - headers: Headers; - setHeaders: (contentType: Headers) => void; -}; - -const HttpHeaderTab: React.FC<Props> = ({ headers, setHeaders }: Props) => { - const add = () => { - setHeaders([...headers, { key: "", value: "" }]); - }; - return ( - <Grid> - <LeftHeader> - <Title>{t`Name`}</Title> - </LeftHeader> - <RightHeader> - <Title>{t`Value`}</Title> - <AddButton primary icon="add" onlyIcon onClick={add} /> - </RightHeader> - {headers.map(({ key, value }, index) => { - const setKey = (key: string) => - setHeaders(assoc(headers, index, { key, value })); - const setValue = (value: string) => - setHeaders(assoc(headers, index, { key, value })); - const remove = () => - setHeaders(assoc(headers, index, false).filter(Boolean)); - return ( - <> - <LeftHeader> - <Header - placeholder={t`Header Name`} - value={key} - setValue={setKey} - /> - </LeftHeader> - <RightHeader> - <Header - placeholder={t`Value`} - value={value} - setValue={setValue} - /> - <DeleteButton icon="trash" onlyIcon onClick={remove} /> - </RightHeader> - </> - ); - })} - </Grid> - ); -}; - -type InputProps = { - value: string; - placeholder: string; - setValue: (value: string) => void; -}; - -const Header: React.FC<InputProps> = ({ - value, - setValue, - placeholder, -}: InputProps) => { - return ( - <Input - placeholder={placeholder} - value={value} - onChange={e => setValue(e.target.value)} - /> - ); -}; - -export default HttpHeaderTab; diff --git a/frontend/src/metabase/writeback/components/HttpAction/JsonEditor/JsonEditor.styled.tsx b/frontend/src/metabase/writeback/components/HttpAction/JsonEditor/JsonEditor.styled.tsx deleted file mode 100644 index 8fca0926a2b814033422629d8e6489d6a507317b..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/JsonEditor/JsonEditor.styled.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import styled from "@emotion/styled"; -import { color } from "metabase/lib/colors"; - -export const Editor = styled.textarea` - width: 100%; - height: 100%; - color: ${color("text-medium")}; - - border: none; - overflow: auto; - outline: none; - - box-shadow: none; - - resize: none; - - &::placeholder { - color: ${color("text-light")}; - } -`; diff --git a/frontend/src/metabase/writeback/components/HttpAction/JsonEditor/JsonEditor.tsx b/frontend/src/metabase/writeback/components/HttpAction/JsonEditor/JsonEditor.tsx deleted file mode 100644 index e00d02f066bbd25ae5184223438220847026d477..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/JsonEditor/JsonEditor.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; - -import { Editor } from "./JsonEditor.styled"; - -type Props = { - value: string; - onChange: (value: string) => void; -}; - -const JsonEditor: React.FC<Props> = ({ value, onChange }: Props) => { - return ( - <Editor - value={value} - onChange={event => onChange(event.target.value)} - placeholder={`{"foo": "{{bar}}"}`} - /> - ); -}; - -export default JsonEditor; diff --git a/frontend/src/metabase/writeback/components/HttpAction/MethodSelector.styled.tsx b/frontend/src/metabase/writeback/components/HttpAction/MethodSelector.styled.tsx deleted file mode 100644 index a2ff28eb0824a13a53b1c14c31e615d6ab5504cd..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/MethodSelector.styled.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import styled from "@emotion/styled"; -import { space } from "metabase/styled-components/theme"; - -export const Container = styled.div` - padding: ${space(0)} ${space(1)}; -`; diff --git a/frontend/src/metabase/writeback/components/HttpAction/MethodSelector.tsx b/frontend/src/metabase/writeback/components/HttpAction/MethodSelector.tsx deleted file mode 100644 index 024d4644a030963b5fe81c87150d8814691c1b1d..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/MethodSelector.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; - -import Radio from "metabase/core/components/Radio"; - -import { Container } from "./MethodSelector.styled"; - -const METHODS = ["GET", "POST", "PUT", "DELETE"].map(method => ({ - name: method, - value: method, -})); - -type Props = { - value: string; - setValue: (value: string) => void; -}; - -const MethodSelector: React.FC<Props> = ({ value, setValue }: Props) => { - return ( - <Container> - <Radio value={value} options={METHODS} onOptionClick={setValue} /> - </Container> - ); -}; - -export default MethodSelector; diff --git a/frontend/src/metabase/writeback/components/HttpAction/ParametersTab.tsx b/frontend/src/metabase/writeback/components/HttpAction/ParametersTab.tsx deleted file mode 100644 index bba1b42b953addadc85afe3c70c0a1bdb3c0eae2..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/ParametersTab.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from "react"; - -import { TemplateTags } from "metabase-types/types/Query"; -import TagEditorParam from "metabase/query_builder/components/template_tags/TagEditorParam"; -import { getDatabasesList } from "metabase/query_builder/selectors"; -import { connect } from "react-redux"; -import { State } from "metabase-types/store"; -import { Database } from "metabase-types/types/Database"; - -type Props = { - templateTags: TemplateTags; - databases: Database[]; - - onTemplateTagsChange: (templateTags: TemplateTags) => void; -}; - -const ParametersTab: React.FC<Props> = ({ - templateTags, - databases, - onTemplateTagsChange, -}: Props) => { - const tags = React.useMemo( - () => Object.values(templateTags || {}), - [templateTags], - ); - const onChange = (templateTag: any) => { - const { name } = templateTag; - const newTag = - templateTags[name] && templateTags[name].type !== templateTag.type - ? // when we switch type, null out any default - { ...templateTag, default: null } - : templateTag; - const newTags = { ...templateTags, [name]: newTag }; - onTemplateTagsChange(newTags); - }; - return ( - <div> - {tags.map(tag => ( - <div key={tag.name}> - <TagEditorParam - // For some reason typescript doesn't think the `tag` prop exists on TagEditorParam - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - tag={tag} - parameter={null} - databaseFields={[]} - database={null} - databases={databases} - setTemplateTag={onChange} - setParameterValue={onChange} - /> - </div> - ))} - </div> - ); -}; - -const mapStateToProps = (state: State) => ({ - databases: getDatabasesList(state), -}); - -export default connect(mapStateToProps)(ParametersTab); diff --git a/frontend/src/metabase/writeback/components/HttpAction/ResponseTab.styled.tsx b/frontend/src/metabase/writeback/components/HttpAction/ResponseTab.styled.tsx deleted file mode 100644 index ef0aa6817c9afc27607333c5320b6048357679e1..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/ResponseTab.styled.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import styled from "@emotion/styled"; -import { color } from "metabase/lib/colors"; -import { space } from "metabase/styled-components/theme"; - -export const Grid = styled.div` - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - grid-template-rows: repeat(3, auto); - grid-gap: ${space(2)}; - - padding: ${space(2)}; -`; - -export const Info = styled.div` - display: flex; - flex-direction: column; -`; - -export const Title = styled.div` - font-size: 1.5rem; - font-weight: 600; - color: ${color("text-dark")}; -`; - -export const Description = styled.div` - font-size: 0.75rem; - font-weight: 400; - color: ${color("text-medium")}; -`; - -export const TextArea = styled.textarea` - color: ${color("text-medium")}; - - border: none; - overflow: auto; - outline: none; - - box-shadow: none; - - resize: none; - - &:focus { - color: ${color("text-dark")}; - border: 1px solid ${color("brand")}; - } - - &::placeholder { - color: ${color("text-light")}; - } -`; - -export const Spacer = styled.div` - grid-column: span 2; -`; diff --git a/frontend/src/metabase/writeback/components/HttpAction/ResponseTab.tsx b/frontend/src/metabase/writeback/components/HttpAction/ResponseTab.tsx deleted file mode 100644 index 8a1a26831e82784f12947cad3d5d8b05fb234de4..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/ResponseTab.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from "react"; -import { t } from "ttag"; - -import { - Grid, - Info, - Title, - TextArea, - Description, - Spacer, -} from "./ResponseTab.styled"; - -type Props = { - responseHandler: string; - onResponseHandlerChange: (responseHandler: string) => void; - - errorHandler: string; - onErrorHandlerChange: (errorHandler: string) => void; -}; - -const ResponseTab: React.FC<Props> = ({ - responseHandler, - onResponseHandlerChange, - errorHandler, - onErrorHandlerChange, -}: Props) => { - return ( - <Grid> - <Info> - <Title>{t`Response Handler`}</Title> - <Description>{t`Specify a JSON path for the response data`}</Description> - </Info> - <TextArea - value={responseHandler} - onChange={event => onResponseHandlerChange(event.target.value)} - placeholder={".body.result"} - /> - <Spacer /> - <Info> - <Title>{t`Error Handler`}</Title> - <Description>{t`Specify a JSON path for the error message`}</Description> - </Info> - <TextArea - value={errorHandler} - onChange={event => onErrorHandlerChange(event.target.value)} - placeholder={".body.error.message"} - /> - </Grid> - ); -}; - -export default ResponseTab; diff --git a/frontend/src/metabase/writeback/components/HttpAction/Tabs.styled.tsx b/frontend/src/metabase/writeback/components/HttpAction/Tabs.styled.tsx deleted file mode 100644 index 19dadefc0850b8769df8ad9057aa38433ab9fc1b..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/Tabs.styled.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import styled from "@emotion/styled"; -import ButtonBase from "metabase/core/components/Button"; -import { color } from "metabase/lib/colors"; -import { space } from "metabase/styled-components/theme"; - -export const Container = styled.div` - display: flex; - - & > * ~ * { - margin-left: ${space(1)}; - margin-right: ${space(1)}; - } -`; - -export const Button = styled(ButtonBase)<{ active: boolean }>` - color: ${props => (props.active ? color("brand") : color("text-light"))}; - padding: ${space(1)} ${space(2)}; - - font-size: 0.875rem; - line-height: 1.25rem; - font-weight: bold; - - &:hover { - color: ${props => (props.active ? color("brand") : color("text-medium"))}; - } -`; diff --git a/frontend/src/metabase/writeback/components/HttpAction/Tabs.tsx b/frontend/src/metabase/writeback/components/HttpAction/Tabs.tsx deleted file mode 100644 index c45402d46b1130f43b4473fbec2587709b16b792..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/Tabs.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from "react"; - -import { Container, Button } from "./Tabs.styled"; - -type Tab = { - name: string; - label: string | React.ReactNode; -}; - -type Props = { - tabs: Tab[]; - currentTab: string; - setCurrentTab: (tab: string) => void; -}; - -const Tabs: React.FC<Props> = ({ tabs, currentTab, setCurrentTab }: Props) => { - return ( - <Container> - {tabs.map(({ name, label }) => ( - <Button - borderless - key={name} - active={currentTab === name} - onClick={() => setCurrentTab(name)} - > - {label} - </Button> - ))} - </Container> - ); -}; - -export default Tabs; diff --git a/frontend/src/metabase/writeback/components/HttpAction/UrlInput.styled.tsx b/frontend/src/metabase/writeback/components/HttpAction/UrlInput.styled.tsx deleted file mode 100644 index b34bb9c6138ebe414d8bc751cbe40b67629b7cf8..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/UrlInput.styled.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import styled from "@emotion/styled"; - -import SelectButton from "metabase/core/components/SelectButton"; - -import { color } from "metabase/lib/colors"; -import { space } from "metabase/styled-components/theme"; - -import CompactSelect from "./CompactSelect"; - -export const Container = styled.div` - display: flex; - align-items: start; -`; - -export const UrlContainer = styled.div` - flex-grow: 1; - width: 100%; - min-height: 0; - padding: 0 ${space(3)} 0 ${space(3)}; - background: transparent; -`; - -export const TextArea = styled.textarea` - font-size: 0.85rem; - width: 100%; - min-height: 0px; - padding: 0 ${space(3)} 0 ${space(1)}; - background: transparent; - border-color: transparent; - - border: none; - overflow: auto; - outline: none; - - box-shadow: none; - resize: none; - - &:focus { - color: ${color("text-dark")}; - border: transparent; - } - - &::placeholder { - color: ${color("text-light")}; - } -`; - -export const Select = styled(CompactSelect)` - background-color: transparent; - - ${SelectButton.Root} { - padding: 0; - background-color: transparent; - } -`; diff --git a/frontend/src/metabase/writeback/components/HttpAction/UrlInput.tsx b/frontend/src/metabase/writeback/components/HttpAction/UrlInput.tsx deleted file mode 100644 index 0e3f205e8a2efa75d56f16260765c05c96a735a4..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/components/HttpAction/UrlInput.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from "react"; - -import { Container, UrlContainer, TextArea, Select } from "./UrlInput.styled"; - -type Props = { - protocol: string; - setProtocol: (protocol: string) => void; - url: string; - setUrl: (url: string) => void; -}; - -const UrlInput: React.FC<Props> = ({ - protocol, - setProtocol, - url, - setUrl, -}: Props) => { - return ( - <div> - <Container> - <UrlContainer> - <TextArea - name="url" - id="url" - rows={2} - wrap="soft" - placeholder="example.com/api/v1/prices" - value={url} - onChange={event => setUrl(event.target.value)} - onKeyDown={event => { - if (event.keyCode === 13 || event.key === "Enter") { - // prevent default behavior - event.preventDefault(); - } - }} - /> - </UrlContainer> - <div> - <Select - value={protocol} - options={[ - { value: "http", name: "HTTP" }, - { value: "https", name: "HTTPS" }, - ]} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => - setProtocol(e.target.value) - } - /> - </div> - </Container> - </div> - ); -}; - -export default UrlInput; diff --git a/frontend/src/metabase/writeback/containers/ActionPage.styled.tsx b/frontend/src/metabase/writeback/containers/ActionPage.styled.tsx deleted file mode 100644 index 4b7e0a5869d65acf631bce4cac24d252792de3db..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/containers/ActionPage.styled.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import styled from "@emotion/styled"; -import { color, alpha, lighten } from "metabase/lib/colors"; - -export const Container = styled.div` - display: flex; - flex-direction: column; - - height: 100%; -`; - -export const Content = styled.div` - flex-grow: 1; - background-color: ${color("white")}; -`; diff --git a/frontend/src/metabase/writeback/containers/CreateActionPage.tsx b/frontend/src/metabase/writeback/containers/CreateActionPage.tsx deleted file mode 100644 index 725d5ab07c9c58be944b98eaf8b9d74a4a37e0fb..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/containers/CreateActionPage.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from "react"; -import { connect } from "react-redux"; - -import Header from "metabase/writeback/components/HttpAction/Header"; -import HttpAction from "metabase/writeback/components/HttpAction/HttpAction"; -import { - createHttpAction, - CreateHttpActionPayload, -} from "metabase/writeback/actions"; -import { getHttpActionTemplateTagParameter } from "metabase/writeback/utils"; -import { ActionType } from "metabase/writeback/types"; - -import { useWritebackAction } from "../hooks"; - -import { Container, Content } from "./ActionPage.styled"; - -type Props = { - createHttpAction: (payload: CreateHttpActionPayload) => void; -}; - -const CreateActionPage: React.FC<Props> = ({ createHttpAction }) => { - const [type, setType] = React.useState<ActionType>("http"); - const { - name, - onNameChange, - description, - onDescriptionChange, - data, - onDataChange, - isDirty, - isValid, - templateTags, - setTemplateTags, - responseHandler, - onResponseHandlerChange, - errorHandler, - onErrorHandlerChange, - } = useWritebackAction({ type }); - - const onCommit = React.useCallback(() => { - if (type === "http") { - const tags = Object.values(templateTags); - const parameters = tags - .filter(tag => tag.type != null) - .map(getHttpActionTemplateTagParameter); - const entity = { - name, - description, - ...data, - template: { - ...data.template, - parameters, - }, - response_handle: responseHandler || null, - error_handle: errorHandler || null, - }; - createHttpAction(entity); - } else { - throw new Error("Action type is not supported"); - } - }, [ - type, - name, - description, - data, - templateTags, - createHttpAction, - responseHandler, - errorHandler, - ]); - - let content = null; - if (type === "http") { - const { template = {} } = data; - content = ( - <HttpAction - data={template} - onDataChange={newData => - onDataChange({ ...data, template: { ...template, ...newData } }) - } - templateTags={templateTags} - onTemplateTagsChange={setTemplateTags} - description={description} - onDescriptionChange={onDescriptionChange} - responseHandler={responseHandler} - onResponseHandlerChange={onResponseHandlerChange} - errorHandler={errorHandler} - onErrorHandlerChange={onErrorHandlerChange} - /> - ); - } - - return ( - <Container> - <Header - name={name} - onNameChange={onNameChange} - type={type} - setType={setType} - canSave={isDirty && isValid} - onCommit={onCommit} - /> - <Content>{content}</Content> - </Container> - ); -}; - -const mapDispatchToProps = (dispatch: any) => ({ - createHttpAction: (payload: CreateHttpActionPayload) => - dispatch(createHttpAction(payload)), -}); - -export default connect(null, mapDispatchToProps)(CreateActionPage); diff --git a/frontend/src/metabase/writeback/containers/EditActionPage.tsx b/frontend/src/metabase/writeback/containers/EditActionPage.tsx deleted file mode 100644 index 0da1bc322cf49f07702a6ba580e6281af4310c8f..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/containers/EditActionPage.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React from "react"; - -import Header from "metabase/writeback/components/HttpAction/Header"; -import HttpAction from "metabase/writeback/components/HttpAction/HttpAction"; -import { WritebackAction } from "metabase/writeback/types"; -import { useWritebackAction } from "../hooks"; -import _ from "underscore"; -import Actions from "metabase/entities/actions"; -import { State } from "metabase-types/store/state"; -import { connect } from "react-redux"; - -import { Container, Content } from "./ActionPage.styled"; -import { getHttpActionTemplateTagParameter } from "../utils"; - -type Props = { - action: WritebackAction; - updateAction: ( - action: WritebackAction, - values: Partial<WritebackAction>, - ) => void; -}; - -const EditActionPage: React.FC<Props> = ({ action, updateAction }: Props) => { - const { - type, - name, - onNameChange, - description, - onDescriptionChange, - data, - onDataChange, - isDirty, - isValid, - templateTags, - setTemplateTags, - responseHandler, - onResponseHandlerChange, - errorHandler, - onErrorHandlerChange, - } = useWritebackAction(action); - - const onCommit = React.useCallback(() => { - if (type === "http") { - const tags = Object.values(templateTags); - const parameters = tags - .filter(tag => tag.type != null) - .map(getHttpActionTemplateTagParameter); - const entity = { - name, - description, - ...data, - template: { - ...data.template, - parameters, - }, - response_handle: responseHandler || null, - error_handle: errorHandler || null, - }; - updateAction(action, entity); - } else { - throw new Error("Action type is not supported"); - } - }, [ - action, - type, - name, - description, - data, - templateTags, - updateAction, - responseHandler, - errorHandler, - ]); - let content = null; - if (type === "http") { - const { template = {} } = data; - content = ( - <HttpAction - data={template} - onDataChange={newData => - onDataChange({ ...data, template: { ...template, ...newData } }) - } - templateTags={templateTags} - onTemplateTagsChange={setTemplateTags} - description={description} - onDescriptionChange={onDescriptionChange} - responseHandler={responseHandler} - onResponseHandlerChange={onResponseHandlerChange} - errorHandler={errorHandler} - onErrorHandlerChange={onErrorHandlerChange} - /> - ); - } - - return ( - <Container> - <Header - name={name} - onNameChange={onNameChange} - type={type} - canSave={isDirty && isValid} - onCommit={onCommit} - /> - <Content>{content}</Content> - </Container> - ); -}; - -const mapDispatchToProps = (dispatch: any) => ({ - updateAction: async ( - action: WritebackAction, - values: Partial<WritebackAction>, - ) => { - await dispatch(Actions.actions.update(action, values)); - }, -}); - -export default _.compose( - connect(null, mapDispatchToProps), - Actions.load({ - id: (_state: State, { params }: { params: { actionId: number } }) => - params.actionId, - wrapped: true, - }), -)(EditActionPage); diff --git a/frontend/src/metabase/writeback/hooks.ts b/frontend/src/metabase/writeback/hooks.ts deleted file mode 100644 index 16cc0ea4b65411d089aa66987effcae128d81acd..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/writeback/hooks.ts +++ /dev/null @@ -1,222 +0,0 @@ -import React from "react"; -import _ from "underscore"; -import { isEqual } from "lodash"; - -import { humanize } from "metabase/lib/formatting"; -import { TemplateTags } from "metabase-types/types/Query"; -import Utils from "metabase/lib/utils"; - -import { ActionType, WritebackAction } from "./types"; -import { recognizeTemplateTags } from "metabase-lib/lib/queries/NativeQuery"; -import { createTemplateTag } from "metabase-lib/lib/queries/TemplateTag"; - -type Data = any; - -export type CreateActionHook = { - type: ActionType; - - name: string; - onNameChange: (name: string) => void; - - description: string; - onDescriptionChange: (description: string) => void; - - data: Data; - onDataChange: (data: Data) => void; - - responseHandler: string; - onResponseHandlerChange: (responseHandler: string) => void; - - errorHandler: string; - onErrorHandlerChange: (errorHandler: string) => void; - - isDirty: boolean; - isValid: boolean; - - templateTags: TemplateTags; - setTemplateTags: (templateTags: TemplateTags) => void; -}; - -const getData = (action: Partial<WritebackAction>): unknown => { - if (action.type === "http") { - const { name, description, ...rest } = action; - return rest || {}; - } else if (action.type === "query") { - return action.card || {}; - } else { - throw new Error("Action type is not supported"); - } -}; - -const getResponseHandler = (action: Partial<WritebackAction>): string => { - if (action.type === "http") { - return action.response_handle || ""; - } else { - throw new Error("Action type is not supported"); - } -}; - -const getErrorHandler = (action: Partial<WritebackAction>): string => { - if (action.type === "http") { - return action.error_handle || ""; - } else { - throw new Error("Action type is not supported"); - } -}; - -export const useWritebackAction = ( - action: Partial<WritebackAction> & { type: ActionType }, -): CreateActionHook => { - const { type } = action; - const [name, setName] = React.useState<string>(action?.name || ""); - const [description, setDescription] = React.useState<string>( - action?.description || "", - ); - const [responseHandler, setResponseHandler] = React.useState<string>( - getResponseHandler(action), - ); - const [errorHandler, setErrorHandler] = React.useState<string>( - getErrorHandler(action), - ); - const [data, setData] = React.useState<Data>(getData(action)); - const [isDirty, setIsDirty] = React.useState<boolean>(false); - - const [templateTags, setTemplateTags] = useTemplateTags(data); - - const isValid = React.useMemo(() => { - if (!name) { - return false; - } - if (type === "http") { - try { - new URL(data.template?.url); - } catch (_) { - return false; - } - return true; - } - return false; - }, [type, data, name]); - - return { - name, - onNameChange: newName => { - if (name !== newName) { - setName(newName); - setIsDirty(true); - } - }, - type, - description, - onDescriptionChange: newDescription => { - if (newDescription !== description) { - setDescription(newDescription); - setIsDirty(true); - } - }, - data, - onDataChange: newData => { - if (!isEqual(data, newData)) { - setData(newData); - setIsDirty(true); - } - }, - isDirty, - isValid, - templateTags, - setTemplateTags: newTags => { - if (!isEqual(templateTags, newTags)) { - setTemplateTags(newTags); - setIsDirty(true); - } - }, - responseHandler, - onResponseHandlerChange: newHandler => { - if (responseHandler !== newHandler) { - setResponseHandler(newHandler); - setIsDirty(true); - } - }, - errorHandler, - onErrorHandlerChange: newHandler => { - if (errorHandler !== newHandler) { - setErrorHandler(newHandler); - setIsDirty(true); - } - }, - }; -}; - -type SetTemplateTags = (tags: TemplateTags) => void; - -// Adapted from NativeQuery._getUpdatedTemplateTags() -export const useTemplateTags = (data: any): [TemplateTags, SetTemplateTags] => { - const [templateTags, setTemplateTags] = React.useState<TemplateTags | null>( - null, - ); - const tags = React.useMemo(() => { - const queryText = JSON.stringify(data); - if (queryText) { - const tags = recognizeTemplateTags(queryText); - const existingTags = Object.keys(templateTags || {}); - - // if we ended up with any variables in the query then update the card parameters list accordingly - if (tags.length > 0 || existingTags.length > 0) { - const newTags = _.difference(tags, existingTags); - - const oldTags: string[] = _.difference(existingTags, tags); - - const newTemplateTags = { ...templateTags }; - - if (oldTags.length === 1 && newTags.length === 1) { - // renaming - const newTag = { ...newTemplateTags[oldTags[0]] }; - - if (newTag["display-name"] === humanize(oldTags[0])) { - newTag["display-name"] = humanize(newTags[0]); - } - - newTag.name = newTags[0]; - newTag.type = "text"; - - newTemplateTags[newTag.name] = newTag; - delete newTemplateTags[oldTags[0]]; - } else { - // remove old vars - for (const name of oldTags) { - delete newTemplateTags[name]; - } - - // create new vars - for (const tagName of newTags) { - newTemplateTags[tagName] = createTemplateTag(tagName); - } - } - - // ensure all tags have an id since we need it for parameter values to work - for (const tag of Object.values(newTemplateTags)) { - if (tag.id == null) { - tag.id = Utils.uuid(); - } - } - - // The logic above is indiscriminant in creating new objects - if (!isEqual(newTemplateTags, templateTags)) { - return newTemplateTags; - } else { - return templateTags; - } - } - } - return INITIAL_TAGS; - }, [data, templateTags]); - - React.useEffect(() => setTemplateTags(tags), [tags]); - - if (templateTags && Object.keys(templateTags).length > 0) { - console.log(templateTags); - } - return [templateTags || INITIAL_TAGS, setTemplateTags]; -}; - -const INITIAL_TAGS = {}; diff --git a/frontend/src/metabase/writeback/utils.ts b/frontend/src/metabase/writeback/utils.ts index eac0618d3ae5167274b75ebb15198c19272f7def..a7f7de3822cdab75c3a7aa069d6594d7e9bfe297 100644 --- a/frontend/src/metabase/writeback/utils.ts +++ b/frontend/src/metabase/writeback/utils.ts @@ -1,19 +1,12 @@ import { TYPE } from "metabase/lib/types"; import { formatSourceForTarget } from "metabase/lib/click-behavior"; -import { - getTemplateTagParameterTarget, - getTemplateTagType, -} from "metabase/parameters/utils/cards"; -import { ParameterWithTarget } from "metabase/parameters/types"; - import Database from "metabase-lib/lib/metadata/Database"; import Field from "metabase-lib/lib/metadata/Field"; import { Database as IDatabase } from "metabase-types/types/Database"; import { DashCard } from "metabase-types/types/Dashboard"; import { Parameter, ParameterId } from "metabase-types/types/Parameter"; -import { TemplateTag } from "metabase-types/types/Query"; import { WritebackAction, @@ -130,19 +123,6 @@ export const getActionEmitterParameterMappings = (action: WritebackAction) => { return parameterMappings; }; -export function getHttpActionTemplateTagParameter( - tag: TemplateTag, -): ParameterWithTarget { - return { - id: tag.id, - type: tag["widget-type"] || getTemplateTagType(tag), - target: getTemplateTagParameterTarget(tag), - name: tag.name, - slug: tag.name, - default: tag.default, - }; -} - export function getActionParameters( parameterMapping: ParametersSourceTargetMap = {}, {