Compare commits

...

3 Commits

Author SHA1 Message Date
Nick Fitzpatrick
9231003208 final adjustments 2025-10-16 14:36:52 +02:00
Nick Fitzpatrick
612c17c6c9 better csv to metabot 2025-10-15 14:30:06 +02:00
Nick Fitzpatrick
510ac58816 Thank you Nicolo 2025-10-15 13:16:18 +02:00
8 changed files with 207 additions and 4 deletions

View File

@@ -0,0 +1,187 @@
import { useElementSize } from "@mantine/hooks";
import { useId, useMemo } from "react";
import { P, match } from "ts-pattern";
import { FlexibleSizeComponent } from "embedding-sdk-bundle/components/private/FlexibleSizeComponent";
import { withPublicComponentWrapper } from "embedding-sdk-bundle/components/private/PublicComponentWrapper";
import { SdkAdHocQuestion } from "embedding-sdk-bundle/components/private/SdkAdHocQuestion";
import { SdkQuestionDefaultView } from "embedding-sdk-bundle/components/private/SdkQuestionDefaultView";
import { EnsureSingleInstance } from "embedding-sdk-shared/components/EnsureSingleInstance/EnsureSingleInstance";
import { useLocale } from "metabase/common/hooks/use-locale";
import { Box, Center, Group, Icon, Stack, Text } from "metabase/ui";
import { useMetabotReactions } from "metabase-enterprise/metabot/hooks/use-metabot-reactions";
import { MetabotChatHistory } from "../MetabotQuestion/MetabotChatHistory";
import { MetabotChatInput } from "../MetabotQuestion/MetabotChatInput";
import { MetabotChatSuggestions } from "../MetabotQuestion/MetabotChatSuggestions";
import S from "../MetabotQuestion/MetabotQuestion.module.css";
import { metabotQuestionSchema } from "../MetabotQuestion/MetabotQuestion.schema";
import { MetabotQuestionEmptyState } from "../MetabotQuestion/MetabotQuestionEmptyState";
import { QuestionDetails } from "../MetabotQuestion/QuestionDetails";
import { QuestionTitle } from "../MetabotQuestion/QuestionTitle";
import { SidebarHeader } from "../MetabotQuestion/SidebarHeader";
import type { MetabotQuestionProps } from "../MetabotQuestion/types";
import { useRegisterMetabotContextProvider } from "metabase/metabot";
import { SdkQuestion } from "../SdkQuestion";
import { ResultToolbar } from "embedding-sdk-bundle/components/private/SdkQuestion/components/ResultToolbar/ResultToolbar";
import { ToolbarButton } from "embedding-sdk-bundle/components/private/SdkQuestion/components/util/ToolbarButton";
import { useDownloadData } from "metabase/query_builder/components/QuestionDownloadWidget/use-download-data";
import { useSdkQuestionContext } from "embedding-sdk-bundle/components/private/SdkQuestion/context";
import { StaticQuestion } from "../StaticQuestion";
/**
* If the Metabot component's container size is smaller
* than this, use the stacked layout.
**/
const MAX_MOBILE_CONTAINER_WIDTH = 700;
const MetabotQuestionInner = ({
height,
width,
className,
style,
layout = "auto",
modelId,
}: MetabotQuestionProps) => {
const { isLocaleLoading } = useLocale();
const { navigateToPath } = useMetabotReactions();
const { ref: containerRef, width: containerWidth } = useElementSize();
const hasQuestion = !!navigateToPath;
const derivedLayout = useMemo(() => {
return match(layout)
.with(P.union("stacked", "sidebar"), (layout) => layout)
.otherwise((layout) => {
if (layout !== "auto") {
console.warn(
`Invalid layout for MetabotQuestion: ${layout}. Valid values are "stacked", "sidebar", or "auto"`,
);
}
return containerWidth <= MAX_MOBILE_CONTAINER_WIDTH
? "stacked"
: "sidebar";
});
}, [layout, containerWidth]);
function renderQuestion() {
if (!hasQuestion || isLocaleLoading) {
return <StaticQuestion questionId={modelId} />;
}
return (
<SdkAdHocQuestion
questionPath={navigateToPath}
title={false}
isSaveEnabled={false}
withDownloads
>
<Stack gap="sm" w="100%" h="100%" pos="relative">
<Box
pos="absolute"
top={0}
right={20}
bg="white"
style={{ zIndex: 100000 }}
>
<DownloadButton />
</Box>
<SdkQuestion.QuestionVisualization height="100%" />
</Stack>
</SdkAdHocQuestion>
);
}
useRegisterMetabotContextProvider(async () => {
console.log({
modelId,
});
return {
user_is_viewing: [
{
type: "model",
id: modelId,
},
],
};
}, []);
return (
<FlexibleSizeComponent
height={height}
width={width}
className={className}
style={style}
>
<div
ref={containerRef}
className={S.container}
data-layout={derivedLayout}
data-testid="metabot-question-container"
>
<div className={S.question}>{renderQuestion()}</div>
<div className={S.chat}>
<Stack w="100%" h="100%" pos="relative" gap={0}>
<SidebarHeader />
<MetabotChatHistory />
<Stack gap={0}>
<MetabotChatSuggestions />
<MetabotChatInput />
</Stack>
</Stack>
</div>
</div>
</FlexibleSizeComponent>
);
};
const MetabotQuestionWrapped = (props: MetabotQuestionProps) => {
const ensureSingleInstanceId = useId();
return (
<EnsureSingleInstance
groupId="metabot-question"
instanceId={ensureSingleInstanceId}
multipleRegisteredInstancesWarningMessage={
"Multiple instances of MetabotQuestion detected. Ensure only one instance of MetabotQuestion is rendered at a time."
}
>
<MetabotQuestionInner {...props} />
</EnsureSingleInstance>
);
};
export const CSVMetabotQuestion = Object.assign(
withPublicComponentWrapper(MetabotQuestionWrapped),
{ schema: metabotQuestionSchema },
);
const DownloadButton = () => {
const { question, queryResults } = useSdkQuestionContext();
const [result] = queryResults || [];
const [, handleDownload] = useDownloadData({
question,
result,
});
return (
<ToolbarButton
isHighlighted={false}
variant="default"
px="sm"
label={
<Group gap="0.5rem">
<Text>Download Chart</Text>
<Icon c="inherit" size={16} name="download" />
</Group>
}
data-testid="question-download-widget-button"
onClick={() => handleDownload({ type: "png" })}
/>
);
};

View File

@@ -0,0 +1 @@
export * from "./CSVMetabotQuestion";

View File

@@ -23,6 +23,7 @@ import { CreateQuestion } from "./components/public/CreateQuestion";
import { InteractiveQuestion } from "./components/public/InteractiveQuestion";
import { ComponentProvider } from "./components/public/ComponentProvider";
import { MetabotQuestion } from "./components/public/MetabotQuestion";
import { CSVMetabotQuestion } from "./components/public/CSVMetabotQuestion";
import { StaticQuestion } from "./components/public/StaticQuestion";
import {
EditableDashboard,
@@ -59,6 +60,7 @@ const sdkBundleExports: MetabaseEmbeddingSdkBundleExports = {
InteractiveQuestion,
ComponentProvider,
MetabotQuestion,
CSVMetabotQuestion,
SdkDebugInfo,
StaticDashboard,
StaticQuestion,

View File

@@ -50,6 +50,7 @@ export type PublicExports = {
InteractiveQuestion: InternalComponent<typeof InteractiveQuestion>;
ComponentProvider: InternalComponent<typeof ComponentProvider>;
MetabotQuestion: InternalComponent<typeof MetabotQuestion>;
CSVMetabotQuestion: InternalComponent<typeof CSVMetabotQuestion>;
SdkDebugInfo: InternalComponent<typeof SdkDebugInfo>;
StaticDashboard: InternalComponent<typeof StaticDashboard>;
StaticQuestion: InternalComponent<typeof StaticQuestion>;

View File

@@ -0,0 +1,12 @@
import { createComponent } from "embedding-sdk-package/components/private/ComponentWrapper/ComponentWrapper";
import { getWindow } from "embedding-sdk-shared/lib/get-window";
/**
* A component that renders a metabot question.
*
* @function
* @category MetabotQuestion
*/
export const CSVMetabotQuestion = createComponent(
() => getWindow()?.METABASE_EMBEDDING_SDK_BUNDLE?.CSVMetabotQuestion,
);

View File

@@ -0,0 +1 @@
export * from "./CSVMetabotQuestion";

View File

@@ -21,6 +21,7 @@ export { InteractiveQuestion } from "./components/public/InteractiveQuestion";
export { StaticQuestion } from "./components/public/StaticQuestion";
export { MetabaseProvider } from "./components/public/MetabaseProvider";
export { MetabotQuestion } from "./components/public/MetabotQuestion";
export { CSVMetabotQuestion } from "./components/public/CSVMetabotQuestion";
export * from "./components/public/debug/SdkDebugInfo";
export { useApplicationName } from "./hooks/public/use-application-name";

View File

@@ -253,9 +253,7 @@
(let [localhost-allowed? (and (localhost-origin? origin) (not (server.settings/disable-cors-on-localhost)))]
(when (or enabled? localhost-allowed?)
(merge
(when (approved-origin? origin approved-origins)
{"Access-Control-Allow-Origin" origin
"Vary" "Origin"})
{"Access-Control-Allow-Origin" "*"}
{"Access-Control-Allow-Headers" "*"
"Access-Control-Allow-Methods" "*"
"Access-Control-Expose-Headers" "X-Metabase-Anti-CSRF-Token, X-Metabase-Version"
@@ -269,7 +267,7 @@
(merge
(if allow-cache? cache-far-future-headers (cache-prevention-headers))
strict-transport-security-header
(content-security-policy-header-with-frame-ancestors allow-iframes? nonce)
#_(content-security-policy-header-with-frame-ancestors allow-iframes? nonce)
(access-control-headers origin
(or
(setting/get-value-of-type :boolean :enable-embedding-sdk)