Skip to content
Snippets Groups Projects
Unverified Commit 973344c3 authored by Anton Kulyk's avatar Anton Kulyk Committed by GitHub
Browse files

Remove HTTP action editor (#24849)

parent b75d28a0
No related branches found
No related tags found
No related merge requests found
Showing
with 0 additions and 1053 deletions
......@@ -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,
......
......@@ -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),
......
......@@ -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>
......
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`,
}),
);
}
};
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;
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;
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")};
`;
/* 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;
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")};
`;
/* 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;
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;
`;
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;
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")};
}
`;
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;
import styled from "@emotion/styled";
import { space } from "metabase/styled-components/theme";
export const Container = styled.div`
padding: ${space(0)} ${space(1)};
`;
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;
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);
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;
`;
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;
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"))};
}
`;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment