Hash static images through the rspack build (#82726)
* Declare png and gif module imports for the bundler * Hash onboarding, upsell and example images through the build * Assert the Slack app icon link against the bundled asset * Hash the upsell card story illustration through the build * Take image sources as props instead of building paths from a base string * Add a ?url image import that the SDK bundle references instead of inlining * Hash empty state, upsell and driver logo images through the build * Hash the map skeleton sprite and the bridge illustration through the build
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import upsellAiCustomizationImage from "assets/img/upsell-ai-customization.png";
|
||||
|
||||
import { MetabotUpsellPage } from "../MetabotUpsellPage";
|
||||
|
||||
export function MetabotCustomizationUpsellPage() {
|
||||
@@ -11,7 +13,7 @@ export function MetabotCustomizationUpsellPage() {
|
||||
title={t`Pick the look and feel of Metabase’s AI agent`}
|
||||
// eslint-disable-next-line metabase/no-literal-metabase-strings -- This upsell refers to the Metabase product.
|
||||
description={t`Customize the name, icon, and illustrations used in AI chat experiences in Metabase.`}
|
||||
image="app/assets/img/upsell-ai-customization.png"
|
||||
image={upsellAiCustomizationImage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import upsellAiUsageControlsImage from "assets/img/upsell-ai-usage-controls.png";
|
||||
|
||||
import { MetabotUpsellPage } from "../MetabotUpsellPage";
|
||||
|
||||
export function MetabotFeatureAccessUpsellPage() {
|
||||
@@ -9,7 +11,7 @@ export function MetabotFeatureAccessUpsellPage() {
|
||||
location="ai-controls-usage-controls-page"
|
||||
title={t`Set AI usage limits and controls`}
|
||||
description={t`Decide which groups can use which AI features, and set sensible limits on usage based on tokens, messages, or dollar spend.`}
|
||||
image="app/assets/img/upsell-ai-usage-controls.png"
|
||||
image={upsellAiUsageControlsImage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import upsellAiSystemPromptsImage from "assets/img/upsell-ai-system-prompts.png";
|
||||
|
||||
import { MetabotUpsellPage } from "../MetabotUpsellPage";
|
||||
|
||||
export function MetabotSystemPromptsUpsellPage() {
|
||||
@@ -9,7 +11,7 @@ export function MetabotSystemPromptsUpsellPage() {
|
||||
location="ai-controls-system-prompts-page"
|
||||
title={t`Write custom instructions for each AI feature`}
|
||||
description={t`SQL code generation, natural language queries, and agent chat can each be customized with your own prompts.`}
|
||||
image="app/assets/img/upsell-ai-system-prompts.png"
|
||||
image={upsellAiSystemPromptsImage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { PropsWithChildren } from "react";
|
||||
import { match } from "ts-pattern";
|
||||
import { t } from "ttag";
|
||||
|
||||
import googleSheetImage from "assets/img/empty-states/google-sheet.svg?url";
|
||||
import { skipToken } from "metabase/api";
|
||||
import {
|
||||
StoragePurchaseButton,
|
||||
@@ -37,7 +38,6 @@ import {
|
||||
Text,
|
||||
Title,
|
||||
} from "metabase/ui";
|
||||
import { getSubpathSafeUrl } from "metabase/urls";
|
||||
import { useGetGsheetsFolderQuery } from "metabase-enterprise/api";
|
||||
import {
|
||||
DriveConnectionDisplay,
|
||||
@@ -59,13 +59,9 @@ const PanelWrapper = ({
|
||||
isModalOpen?: boolean;
|
||||
onModalClose?: () => void;
|
||||
}>) => {
|
||||
const illustration = getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/google-sheet.svg",
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack gap="lg" align="center" justify="center" pt="2.5rem">
|
||||
<Center component="img" src={illustration} w="3rem" />
|
||||
<Center component="img" src={googleSheetImage} w="3rem" />
|
||||
<Box component="header" ta="center" maw={CONTENT_MAX_WIDTH}>
|
||||
<Title order={2} size="h4" mb="sm">
|
||||
{title}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import upsellAiUsageAuditingImage from "assets/img/upsell-ai-usage-auditing.png";
|
||||
import { DottedBackground } from "metabase/common/components/upsells/components/DottedBackground";
|
||||
import { LineDecorator } from "metabase/common/components/upsells/components/LineDecorator";
|
||||
import { useUpgradeAction } from "metabase/common/components/upsells/components/UpgradeModal";
|
||||
@@ -31,7 +32,7 @@ export function MetabotAnalyticsUpsellPage() {
|
||||
description={t`Track trends of token use, conversations, and messages, and understand what your team is doing with AI in Metabase.`}
|
||||
upgradeOnClick={upgradeOnClick}
|
||||
upgradeUrl={upgradeUrl}
|
||||
image="app/assets/img/upsell-ai-usage-auditing.png"
|
||||
image={upsellAiUsageAuditingImage}
|
||||
/>
|
||||
</LineDecorator>
|
||||
</Stack>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import cx from "classnames";
|
||||
import { t } from "ttag";
|
||||
|
||||
import attributesIllustration from "assets/img/attributes_illustration.png";
|
||||
import attributesIllustration2x from "assets/img/attributes_illustration@2x.png";
|
||||
import CS from "metabase/css/core/index.css";
|
||||
|
||||
interface AttributeOptionsEmptyStateProps {
|
||||
@@ -20,11 +22,8 @@ export const AttributeOptionsEmptyState = ({
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src="app/assets/img/attributes_illustration.png"
|
||||
srcSet="
|
||||
app/assets/img/attributes_illustration.png 1x,
|
||||
app/assets/img/attributes_illustration@2x.png 2x,
|
||||
"
|
||||
src={attributesIllustration}
|
||||
srcSet={`${attributesIllustration} 1x, ${attributesIllustration2x} 2x`}
|
||||
className={CS.mr2}
|
||||
/>
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import noResultsSource from "assets/img/no_results.svg";
|
||||
import bridgeImage from "img/bridge.svg?url";
|
||||
import type { IllustrationValue } from "metabase/plugins";
|
||||
import type { State } from "metabase/redux/store";
|
||||
import {
|
||||
@@ -59,7 +60,7 @@ export function getLoginPageIllustration(state: State): IllustrationValue {
|
||||
switch (illustrationOption) {
|
||||
case "default":
|
||||
return {
|
||||
src: "app/img/bridge.svg",
|
||||
src: bridgeImage,
|
||||
isDefault: true,
|
||||
};
|
||||
|
||||
@@ -86,7 +87,7 @@ export function getLandingPageIllustration(state: State): IllustrationValue {
|
||||
switch (illustrationOption) {
|
||||
case "default":
|
||||
return {
|
||||
src: "app/img/bridge.svg",
|
||||
src: bridgeImage,
|
||||
isDefault: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import metabotSlackbotIcon from "assets/img/metabot-slackbot.png";
|
||||
import { ExternalLink } from "metabase/common/components/ExternalLink";
|
||||
import { useSetting } from "metabase/settings";
|
||||
import { Box, Button, Flex, Stack, Text } from "metabase/ui";
|
||||
@@ -12,7 +13,7 @@ export const SlackConfiguration = () => {
|
||||
const bugReportingEnabled = useSetting("bug-reporting-enabled") ?? false;
|
||||
const { data: appInfo } = useGetSlackAppInfoQuery();
|
||||
|
||||
const iconUrl = "/app/assets/img/metabot-slackbot.png";
|
||||
const iconUrl = metabotSlackbotIcon;
|
||||
const basicInfoUrl = appInfo?.app_id
|
||||
? `https://api.slack.com/apps/${appInfo.app_id}/general#edit`
|
||||
: `https://api.slack.com/apps`;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "__support__/server-mocks";
|
||||
import { createMockSettingsState } from "__support__/state";
|
||||
import { renderWithProviders, screen, waitFor } from "__support__/ui";
|
||||
import metabotSlackbotIcon from "assets/img/metabot-slackbot.png";
|
||||
import { createMockSettings } from "metabase-types/api/mocks";
|
||||
|
||||
import { SlackSetup } from "./SlackSetup";
|
||||
@@ -151,10 +152,8 @@ describe("SlackSetup", () => {
|
||||
const downloadLink = screen.getByRole("link", {
|
||||
name: "Download App Icon",
|
||||
});
|
||||
expect(downloadLink).toHaveAttribute(
|
||||
"href",
|
||||
"/app/assets/img/metabot-slackbot.png",
|
||||
);
|
||||
expect(downloadLink).toHaveAttribute("href", metabotSlackbotIcon);
|
||||
expect(downloadLink).toHaveAttribute("download", "metabot-slack-icon.png");
|
||||
});
|
||||
|
||||
it("should show link to Slack app settings", async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { c, t } from "ttag";
|
||||
|
||||
import upsellCloudImage from "assets/img/upsell-cloud.png?url";
|
||||
import { ExternalLink } from "metabase/common/components/ExternalLink";
|
||||
import { UpsellBigCard } from "metabase/common/components/upsells/components";
|
||||
import S from "metabase/common/components/upsells/components/Upsells.module.css";
|
||||
@@ -25,7 +26,7 @@ export const UpsellCloud = ({
|
||||
buttonText={t`Try for free`}
|
||||
source={source}
|
||||
onClick={onOpenModal}
|
||||
illustrationSrc="app/assets/img/upsell-cloud.png"
|
||||
illustrationSrc={upsellCloudImage}
|
||||
>
|
||||
{c("'restores' and 'upgrades' are nouns in plural in this context")
|
||||
.t`Get automatic backups, restores, and upgrades, built-in network monitoring, unlimited expert help from engineers and more.`}{" "}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import upsellDataAppsImage from "assets/img/upsell-data-apps.png?url";
|
||||
import { UpsellBigCard } from "metabase/common/components/upsells/components";
|
||||
import { UPGRADE_URL } from "metabase/common/components/upsells/constants";
|
||||
import { useHasTokenFeature } from "metabase/common/hooks";
|
||||
@@ -24,7 +25,7 @@ export const UpsellDataApps = ({ source }: { source: string }) => {
|
||||
buttonText={t`Try for free`}
|
||||
buttonLink={UPGRADE_URL}
|
||||
source={source}
|
||||
illustrationSrc="app/assets/img/upsell-data-apps.png"
|
||||
illustrationSrc={upsellDataAppsImage}
|
||||
onClick={triggerUpsellFlow}
|
||||
>
|
||||
<span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import upsellThemesImage from "assets/img/upsell-themes.png?url";
|
||||
import { DottedBackground } from "metabase/common/components/upsells/components/DottedBackground";
|
||||
import { LineDecorator } from "metabase/common/components/upsells/components/LineDecorator";
|
||||
import { useUpgradeAction } from "metabase/common/components/upsells/components/UpgradeModal";
|
||||
@@ -33,7 +34,7 @@ export const UpsellEmbeddingTheme = ({ source }: { source: string }) => {
|
||||
description={t`Fine-tune the appearance of your embedded content with colors and fonts.`}
|
||||
upgradeOnClick={upgradeOnClick}
|
||||
upgradeUrl={upgradeUrl}
|
||||
image="app/assets/img/upsell-themes.png"
|
||||
image={upsellThemesImage}
|
||||
/>
|
||||
</LineDecorator>
|
||||
</Stack>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import upsellRemoteSyncImage from "assets/img/upsell-remote-sync.png?url";
|
||||
import { ExternalLink } from "metabase/common/components/ExternalLink";
|
||||
import { UpsellBigCard } from "metabase/common/components/upsells/components";
|
||||
import S from "metabase/common/components/upsells/components/Upsells.module.css";
|
||||
@@ -33,7 +34,7 @@ export const UpsellRemoteSync = ({ source }: { source: string }) => {
|
||||
buttonLink={UPGRADE_URL}
|
||||
source={source}
|
||||
onClick={triggerUpsellFlow}
|
||||
illustrationSrc="app/assets/img/upsell-remote-sync.png"
|
||||
illustrationSrc={upsellRemoteSyncImage}
|
||||
>
|
||||
{t`Keep your most important datasets, metrics, and SQL logic under version control. Sync content to a Git repository to review changes, collaborate, and maintain a production-ready source of truth.`}
|
||||
<ExternalLink
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import upsellTenantsImage from "assets/img/upsell-tenants.png?url";
|
||||
import { DottedBackground } from "metabase/common/components/upsells/components/DottedBackground";
|
||||
import { LineDecorator } from "metabase/common/components/upsells/components/LineDecorator";
|
||||
import { useUpgradeAction } from "metabase/common/components/upsells/components/UpgradeModal";
|
||||
@@ -33,7 +34,7 @@ export const UpsellTenants = () => {
|
||||
description={t`Group your customers into tenants, reuse the same dashboards and permissions, and keep each tenant's data isolated.`}
|
||||
upgradeOnClick={upgradeOnClick}
|
||||
upgradeUrl={upgradeUrl}
|
||||
image="app/assets/img/upsell-tenants.png"
|
||||
image={upsellTenantsImage}
|
||||
/>
|
||||
</LineDecorator>
|
||||
</Stack>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import upsellWhitelabelImage from "assets/img/upsell-whitelabel.png?url";
|
||||
import { ExternalLink } from "metabase/common/components/ExternalLink";
|
||||
import { UpsellBigCard } from "metabase/common/components/upsells/components";
|
||||
import S from "metabase/common/components/upsells/components/Upsells.module.css";
|
||||
@@ -43,7 +44,7 @@ export const UpsellWhitelabel = ({ source }: { source: string }) => {
|
||||
buttonText={t`Try for free`}
|
||||
buttonLink={UPGRADE_URL}
|
||||
source={source}
|
||||
illustrationSrc="app/assets/img/upsell-whitelabel.png"
|
||||
illustrationSrc={upsellWhitelabelImage}
|
||||
onClick={triggerUpsellFlow}
|
||||
>
|
||||
{t`Customize your internal or customer-facing analytics with your brand name, logo, colors, font and more, and hide giveaway Metabase elements.`}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import cx from "classnames";
|
||||
import { t } from "ttag";
|
||||
|
||||
import browseModelsVideoThumbnail from "assets/img/browse-models-video-thumbnail.png";
|
||||
|
||||
import S from "./ModelsVideo.module.css";
|
||||
|
||||
export const ModelsVideo = ({ autoplay }: { autoplay: 0 | 1 }) => {
|
||||
@@ -27,7 +29,7 @@ export const ModelsVideoThumbnail = ({ onClick }: { onClick: () => void }) => {
|
||||
<img
|
||||
alt={t`Browse models video thumbnail`}
|
||||
loading="lazy"
|
||||
src="app/assets/img/browse-models-video-thumbnail.png"
|
||||
src={browseModelsVideoThumbnail}
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { EmptyState } from "metabase/common/components/EmptyState";
|
||||
import {
|
||||
EmptyState,
|
||||
type ImageSource,
|
||||
} from "metabase/common/components/EmptyState";
|
||||
import { getUser } from "metabase/current-user";
|
||||
import { useSelector } from "metabase/redux";
|
||||
import type { IconName } from "metabase-types/api";
|
||||
@@ -14,7 +17,7 @@ interface AdminAwareEmptyStateProps {
|
||||
message?: ReactNode;
|
||||
adminMessage?: ReactNode;
|
||||
icon?: IconName;
|
||||
image?: string;
|
||||
image?: ImageSource;
|
||||
action?: ReactNode;
|
||||
adminAction?: ReactNode;
|
||||
link?: string;
|
||||
|
||||
@@ -16,6 +16,14 @@ import type { IconName } from "metabase-types/api";
|
||||
|
||||
import S from "./EmptyState.module.css";
|
||||
|
||||
/**
|
||||
* A raster illustration and its high-density variant, both resolved by the bundler.
|
||||
*/
|
||||
export type ImageSource = {
|
||||
src: string;
|
||||
srcSet?: string;
|
||||
};
|
||||
|
||||
// Don't break existing empty states
|
||||
// TODO - remove these and update empty states with proper usage of illustrationElement
|
||||
const LegacyIcon = ({ icon }: { icon: IconName }) =>
|
||||
@@ -27,21 +35,20 @@ const LegacyImage = ({
|
||||
imageClassName,
|
||||
message,
|
||||
}: {
|
||||
image: string;
|
||||
image: ImageSource;
|
||||
imageHeight?: number;
|
||||
imageClassName?: string;
|
||||
message?: string;
|
||||
}) =>
|
||||
image ? (
|
||||
<img
|
||||
src={`${image}.png`}
|
||||
width="300px"
|
||||
height={imageHeight}
|
||||
alt={message}
|
||||
srcSet={`${image}@2x.png 2x`}
|
||||
className={imageClassName}
|
||||
/>
|
||||
) : null;
|
||||
}) => (
|
||||
<img
|
||||
src={image.src}
|
||||
width="300px"
|
||||
height={imageHeight}
|
||||
alt={message}
|
||||
srcSet={image.srcSet}
|
||||
className={imageClassName}
|
||||
/>
|
||||
);
|
||||
|
||||
type EmptyStateProps = {
|
||||
message?: React.ReactNode;
|
||||
@@ -52,7 +59,7 @@ type EmptyStateProps = {
|
||||
onActionClick?: () => void;
|
||||
className?: string;
|
||||
icon?: IconName;
|
||||
image?: string;
|
||||
image?: ImageSource;
|
||||
spacing?: "sm" | "md";
|
||||
actionVariant?: ButtonProps["variant"];
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { EmptyState } from "./EmptyState";
|
||||
export { EmptyState, type ImageSource } from "./EmptyState";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import databasesListImage from "assets/img/databases-list.png";
|
||||
import databasesListImage2x from "assets/img/databases-list@2x.png";
|
||||
import { AdminAwareEmptyState } from "metabase/common/components/AdminAwareEmptyState";
|
||||
import { useSelector } from "metabase/redux";
|
||||
import { getApplicationName } from "metabase/selectors/whitelabel";
|
||||
@@ -11,7 +13,10 @@ export const NoDatabasesEmptyState = () => {
|
||||
title={t`${applicationName} is no fun without any data`}
|
||||
adminMessage={t`Your databases will appear here once you connect one`}
|
||||
message={t`Databases will appear here once your admins have added some`}
|
||||
image="app/assets/img/databases-list"
|
||||
image={{
|
||||
src: databasesListImage,
|
||||
srcSet: `${databasesListImage2x} 2x`,
|
||||
}}
|
||||
adminAction={t`Connect a database`}
|
||||
adminLink="/admin/databases/create"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import usageAnalyticsIllustrationSource from "assets/img/usage-analytics.png?url";
|
||||
import { ExternalLink } from "metabase/common/components/ExternalLink";
|
||||
import { PLUGIN_ADMIN_SETTINGS } from "metabase/plugins";
|
||||
import { useSelector } from "metabase/redux";
|
||||
@@ -9,8 +10,6 @@ import { Box, type BoxProps, Text } from "metabase/ui";
|
||||
import { UpsellCard, type UpsellCardProps } from "./UpsellCard";
|
||||
import { UPGRADE_URL } from "./constants";
|
||||
|
||||
const usageAnalyticsIllustrationSource = "app/assets/img/usage-analytics.png";
|
||||
|
||||
export const UpsellUsageAnalytics = (
|
||||
props: BoxProps &
|
||||
Omit<
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { ReduxProvider } from "__support__/storybook";
|
||||
import upsellPerformanceToolsImage from "assets/img/upsell-performance-tools.png";
|
||||
import { ExternalLink } from "metabase/common/components/ExternalLink";
|
||||
import { Box, Modal } from "metabase/ui";
|
||||
|
||||
@@ -14,7 +15,7 @@ const args = {
|
||||
campaign: "upsell-big-card",
|
||||
source: "storybook",
|
||||
title: "Troubleshoot faster",
|
||||
illustrationSrc: "app/assets/img/upsell-performance-tools.png",
|
||||
illustrationSrc: upsellPerformanceToolsImage,
|
||||
} as const;
|
||||
|
||||
const argTypes = {
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export const LOAD_COMPLETE_FAVICON = "app/assets/img/blue_check.png";
|
||||
import blueCheckImage from "assets/img/blue_check.png?url";
|
||||
|
||||
export const LOAD_COMPLETE_FAVICON = blueCheckImage;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import favicon32Image from "assets/img/favicon-32x32.png";
|
||||
|
||||
const hasNotificationAPI = "Notification" in window;
|
||||
|
||||
export function useWebNotification() {
|
||||
@@ -18,7 +20,7 @@ export function useWebNotification() {
|
||||
|
||||
const notification = new Notification(title, {
|
||||
body,
|
||||
icon: "app/assets/img/favicon-32x32.png",
|
||||
icon: favicon32Image,
|
||||
});
|
||||
|
||||
const closeNotification = (e: Event) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import dependenciesUpsellImage from "assets/img/data-studio-dependencies-upsell.png";
|
||||
import { usePageTitle } from "metabase/hooks/use-page-title";
|
||||
|
||||
import { BaseUpsellPage } from "./BaseUpsellPage";
|
||||
@@ -14,7 +15,7 @@ export function DependenciesUpsellPage() {
|
||||
header={t`Dependency graph`}
|
||||
title={t`See how everything connects`}
|
||||
description={t`Better manage your data transformation and entity graph by inspecting it visually, understanding relationships and dependencies.`}
|
||||
image="app/assets/img/data-studio-dependencies-upsell.png"
|
||||
image={dependenciesUpsellImage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import libraryUpsellImage from "assets/img/data-studio-library-upsell.svg";
|
||||
|
||||
import { BaseUpsellPage } from "./BaseUpsellPage";
|
||||
|
||||
export function LibraryUpsellPage() {
|
||||
@@ -10,7 +12,7 @@ export function LibraryUpsellPage() {
|
||||
header={t`Semantic layer`}
|
||||
title={t`Bring more order to your analytics`}
|
||||
description={t`Create a shared semantic layer of datasets and metrics your team can rely on, so things stay consistent as your data and downstream content grows.`}
|
||||
image="app/assets/img/data-studio-library-upsell.svg"
|
||||
image={libraryUpsellImage}
|
||||
variant="image-full-height"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import remoteSyncUpsellImage from "assets/img/data-studio-remote-sync-upsell.svg";
|
||||
import { DottedBackground } from "metabase/common/components/upsells/components/DottedBackground";
|
||||
import { LineDecorator } from "metabase/common/components/upsells/components/LineDecorator";
|
||||
import { useUpgradeAction } from "metabase/common/components/upsells/components/UpgradeModal";
|
||||
@@ -36,7 +37,7 @@ export function RemoteSyncUpsellPage() {
|
||||
location={LOCATION}
|
||||
title={t`Manage your ${applicationName} content in Git`}
|
||||
description={t`Keep your most important datasets, metrics, and SQL logic under version control. Sync content to a Git repository to review changes, collaborate, and maintain a production-ready source of truth.`}
|
||||
image="app/assets/img/data-studio-remote-sync-upsell.svg"
|
||||
image={remoteSyncUpsellImage}
|
||||
upgradeOnClick={upgradeOnClick}
|
||||
upgradeUrl={upgradeUrl}
|
||||
variant="image-full-height"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import schemaViewerUpsellImage from "assets/img/schema-viewer.svg";
|
||||
import { usePageTitle } from "metabase/hooks/use-page-title";
|
||||
|
||||
import { BaseUpsellPage } from "./BaseUpsellPage";
|
||||
@@ -14,7 +15,7 @@ export function SchemaViewerUpsellPage() {
|
||||
header={t`Schema viewer`}
|
||||
title={t`Visualize your database structure`}
|
||||
description={t`Explore tables and their relationships at a glance. Follow foreign keys, expand into related tables, and see how your schema fits together.`}
|
||||
image="app/assets/img/schema-viewer.svg"
|
||||
image={schemaViewerUpsellImage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import athenaLogo from "assets/img/drivers/athena.svg?url";
|
||||
import bigqueryLogo from "assets/img/drivers/bigquery.svg?url";
|
||||
import clickhouseLogo from "assets/img/drivers/clickhouse.svg?url";
|
||||
import databricksLogo from "assets/img/drivers/databricks.svg?url";
|
||||
import druidLogo from "assets/img/drivers/druid.svg?url";
|
||||
import h2Logo from "assets/img/drivers/h2.svg?url";
|
||||
import mongoLogo from "assets/img/drivers/mongo.svg?url";
|
||||
import mysqlLogo from "assets/img/drivers/mysql.svg?url";
|
||||
import oracleLogo from "assets/img/drivers/oracle.svg?url";
|
||||
import postgresLogo from "assets/img/drivers/postgres.svg?url";
|
||||
import prestoLogo from "assets/img/drivers/presto.svg?url";
|
||||
import redshiftLogo from "assets/img/drivers/redshift.svg?url";
|
||||
import snowflakeLogo from "assets/img/drivers/snowflake.svg?url";
|
||||
import sparksqlLogo from "assets/img/drivers/sparksql.svg?url";
|
||||
import sqliteLogo from "assets/img/drivers/sqlite.svg?url";
|
||||
import sqlserverLogo from "assets/img/drivers/sqlserver.svg?url";
|
||||
import starburstLogo from "assets/img/drivers/starburst.svg?url";
|
||||
import verticaLogo from "assets/img/drivers/vertica.svg?url";
|
||||
import { SAVED_QUESTIONS_VIRTUAL_DB_ID } from "metabase-lib/v1/metadata/utils/saved-questions";
|
||||
|
||||
import DatabaseAuthCodeDescription from "./components/DatabaseAuthCodeDescription";
|
||||
@@ -46,27 +64,27 @@ export const ELEVATED_ENGINES = [
|
||||
export const ALLOWED_ENGINES_FOR_TABLE_EDITING = ["postgres", "mysql"];
|
||||
|
||||
export const ENGINE_LOGO: Record<string, string> = {
|
||||
athena: "athena.svg",
|
||||
bigquery: "bigquery.svg",
|
||||
"bigquery-cloud-sdk": "bigquery.svg",
|
||||
clickhouse: "clickhouse.svg",
|
||||
databricks: "databricks.svg",
|
||||
druid: "druid.svg",
|
||||
"druid-jdbc": "druid.svg",
|
||||
h2: "h2.svg",
|
||||
mongo: "mongo.svg",
|
||||
mysql: "mysql.svg",
|
||||
oracle: "oracle.svg",
|
||||
postgres: "postgres.svg",
|
||||
presto: "presto.svg",
|
||||
"presto-jdbc": "presto.svg",
|
||||
redshift: "redshift.svg",
|
||||
snowflake: "snowflake.svg",
|
||||
sparksql: "sparksql.svg",
|
||||
starburst: "starburst.svg",
|
||||
sqlite: "sqlite.svg",
|
||||
sqlserver: "sqlserver.svg",
|
||||
vertica: "vertica.svg",
|
||||
athena: athenaLogo,
|
||||
bigquery: bigqueryLogo,
|
||||
"bigquery-cloud-sdk": bigqueryLogo,
|
||||
clickhouse: clickhouseLogo,
|
||||
databricks: databricksLogo,
|
||||
druid: druidLogo,
|
||||
"druid-jdbc": druidLogo,
|
||||
h2: h2Logo,
|
||||
mongo: mongoLogo,
|
||||
mysql: mysqlLogo,
|
||||
oracle: oracleLogo,
|
||||
postgres: postgresLogo,
|
||||
presto: prestoLogo,
|
||||
"presto-jdbc": prestoLogo,
|
||||
redshift: redshiftLogo,
|
||||
snowflake: snowflakeLogo,
|
||||
sparksql: sparksqlLogo,
|
||||
starburst: starburstLogo,
|
||||
sqlite: sqliteLogo,
|
||||
sqlserver: sqlserverLogo,
|
||||
vertica: verticaLogo,
|
||||
};
|
||||
|
||||
export const ADVANCED_FIELDS = [
|
||||
|
||||
@@ -40,8 +40,7 @@ const getEngineOption = (engineKey: string, engine: Engine) => {
|
||||
};
|
||||
|
||||
export const getEngineLogo = (engine: string): string | undefined => {
|
||||
const logo = ENGINE_LOGO[engine];
|
||||
return logo ? `app/assets/img/drivers/${logo}` : undefined;
|
||||
return ENGINE_LOGO[engine];
|
||||
};
|
||||
|
||||
export const getDefaultEngineKey = (engines: Record<string, Engine>) => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useCallback } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
import multiTenantUserStrategyImage from "assets/img/embedding-onboarding/multi-tenant-user-strategy.svg";
|
||||
import {
|
||||
useCreateCollectionMutation,
|
||||
useListCollectionsTreeQuery,
|
||||
@@ -65,7 +66,7 @@ export const EnableTenantsStepContent = ({
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
<img
|
||||
src="app/assets/img/embedding-onboarding/multi-tenant-user-strategy.svg"
|
||||
src={multiTenantUserStrategyImage}
|
||||
alt=""
|
||||
className={S.illustration}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@ import styled from "@emotion/styled";
|
||||
import { match } from "ts-pattern";
|
||||
import { t } from "ttag";
|
||||
|
||||
import embedJsExampleImage from "assets/img/embed-js-example.png";
|
||||
import { ExternalLink } from "metabase/common/components/ExternalLink";
|
||||
import { Link } from "metabase/common/components/Link";
|
||||
import { Box, Button, Group, Text } from "metabase/ui";
|
||||
@@ -48,7 +49,7 @@ export const EmbedJsContent = ({
|
||||
</Text>
|
||||
{showImage && (
|
||||
<EmbedJsImage
|
||||
src="/app/assets/img/embed-js-example.png"
|
||||
src={embedJsExampleImage}
|
||||
alt="Modular embedding example"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import styled from "@emotion/styled";
|
||||
import { t } from "ttag";
|
||||
|
||||
import staticEmbeddingExampleImage from "assets/img/static-embedding-example.png";
|
||||
import { ExternalLink } from "metabase/common/components/ExternalLink";
|
||||
import { Link } from "metabase/common/components/Link";
|
||||
import { Box, Button, Group, Text } from "metabase/ui";
|
||||
@@ -33,7 +34,7 @@ export const StaticEmbedContent = ({
|
||||
</Text>
|
||||
{showImage && (
|
||||
<StaticEmbedImage
|
||||
src="/app/assets/img/static-embedding-example.png"
|
||||
src={staticEmbeddingExampleImage}
|
||||
alt="Static embedding example"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import onboardingAiImage from "assets/img/onboarding_ai.png";
|
||||
import onboardingAiImage2x from "assets/img/onboarding_ai@2x.png";
|
||||
import { useSelector } from "metabase/redux";
|
||||
import { getApplicationName } from "metabase/selectors/whitelabel";
|
||||
import { Text } from "metabase/ui";
|
||||
@@ -36,8 +38,8 @@ export const AiItem = ({ value, itemRef }: OnboardingItemProps) => {
|
||||
>
|
||||
<ChecklistImage
|
||||
alt={t`Connecting an AI provider in the admin settings`}
|
||||
src="app/assets/img/onboarding_ai.png"
|
||||
srcSet="app/assets/img/onboarding_ai@2x.png 2x"
|
||||
src={onboardingAiImage}
|
||||
srcSet={`${onboardingAiImage2x} 2x`}
|
||||
style={ILLUSTRATION_STYLE}
|
||||
/>
|
||||
<Text>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { jt, t } from "ttag";
|
||||
|
||||
import onboardingDataStudioImage from "assets/img/onboarding_data_studio.png";
|
||||
import onboardingDataStudioImage2x from "assets/img/onboarding_data_studio@2x.png";
|
||||
import { canAccessDataStudio } from "metabase/common/data-studio/selectors";
|
||||
import { useSelector } from "metabase/redux";
|
||||
import { Text } from "metabase/ui";
|
||||
@@ -42,8 +44,8 @@ export const DataStudioItem = ({ value, itemRef }: OnboardingItemProps) => {
|
||||
>
|
||||
<ChecklistImage
|
||||
alt={t`A table in the Data Studio library`}
|
||||
src="app/assets/img/onboarding_data_studio.png"
|
||||
srcSet="app/assets/img/onboarding_data_studio@2x.png 2x"
|
||||
src={onboardingDataStudioImage}
|
||||
srcSet={`${onboardingDataStudioImage2x} 2x`}
|
||||
style={ILLUSTRATION_STYLE}
|
||||
/>
|
||||
<Text>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import onboardingDataDiagramImage from "assets/img/onboarding_data_diagram.png";
|
||||
import onboardingDataDiagramImage2x from "assets/img/onboarding_data_diagram@2x.png";
|
||||
import { useSelector } from "metabase/redux";
|
||||
import { getApplicationName } from "metabase/selectors/whitelabel";
|
||||
import { Text } from "metabase/ui";
|
||||
@@ -26,8 +28,8 @@ export const DatabaseItem = ({ value, itemRef }: OnboardingItemProps) => {
|
||||
>
|
||||
<ChecklistImage
|
||||
alt={`${applicationName} ${t`data stack`}`}
|
||||
src="app/assets/img/onboarding_data_diagram.png"
|
||||
srcSet="app/assets/img/onboarding_data_diagram@2x.png 2x"
|
||||
src={onboardingDataDiagramImage}
|
||||
srcSet={`${onboardingDataDiagramImage2x} 2x`}
|
||||
/>
|
||||
<Text>
|
||||
{t`Connect one or more databases. You can query these databases directly, either with the query builder or the native SQL editor.`}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import onboardingInviteImage from "assets/img/onboarding_invite.png";
|
||||
import onboardingInviteImage2x from "assets/img/onboarding_invite@2x.png";
|
||||
import { useSelector } from "metabase/redux";
|
||||
import { getApplicationName } from "metabase/selectors/whitelabel";
|
||||
import { Text } from "metabase/ui";
|
||||
@@ -40,8 +42,8 @@ export const InviteItem = ({ value, itemRef }: OnboardingItemProps) => {
|
||||
>
|
||||
<ChecklistImage
|
||||
alt={t`Admin panel with the "Invite someone" button`}
|
||||
src="app/assets/img/onboarding_invite.png"
|
||||
srcSet="app/assets/img/onboarding_invite@2x.png 2x"
|
||||
src={onboardingInviteImage}
|
||||
srcSet={`${onboardingInviteImage2x} 2x`}
|
||||
style={ILLUSTRATION_STYLE}
|
||||
/>
|
||||
<Text>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import lineChartImage from "assets/img/empty-states/visualizations/line.svg?url";
|
||||
import CS from "metabase/css/core/index.css";
|
||||
import { Box, Center, Stack, Text, Title } from "metabase/ui";
|
||||
import { getSubpathSafeUrl } from "metabase/urls";
|
||||
|
||||
function MetricsViewerEmptyStateLayout({
|
||||
title,
|
||||
@@ -11,17 +11,13 @@ function MetricsViewerEmptyStateLayout({
|
||||
title: string;
|
||||
description: string;
|
||||
}): JSX.Element {
|
||||
const imgSrc = getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/line.svg",
|
||||
);
|
||||
|
||||
return (
|
||||
<Center flex={1}>
|
||||
<Stack>
|
||||
<Box maw="20rem" mb="1.5rem">
|
||||
<img
|
||||
className={CS.pointerEventsNone}
|
||||
src={imgSrc}
|
||||
src={lineChartImage}
|
||||
alt={t`Line chart illustration`}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ImageSource } from "metabase/common/components/EmptyState";
|
||||
import { Link, type LinkProps } from "metabase/common/components/Link";
|
||||
import { Box, Flex, Text, Title } from "metabase/ui";
|
||||
|
||||
@@ -6,7 +7,7 @@ import S from "./NewModelOption.module.css";
|
||||
const DEFAULT_IMAGE_WIDTH = 210;
|
||||
|
||||
type NewModelOptionProps = LinkProps & {
|
||||
image: string;
|
||||
image: ImageSource;
|
||||
title: string;
|
||||
description: string;
|
||||
width?: number;
|
||||
@@ -22,7 +23,7 @@ export function NewModelOption({
|
||||
return (
|
||||
<Link {...props} className={S.linkWrapper}>
|
||||
<Flex align="center" justify="center" h="10rem">
|
||||
<img src={`${image}.png`} srcSet={`${image}@2x.png 2x`} width={width} />
|
||||
<img src={image.src} srcSet={image.srcSet} width={width} />
|
||||
</Flex>
|
||||
<Box my="lg">
|
||||
<Title order={2} className={S.modelTitle}>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Route } from "metabase/router";
|
||||
import { NewModelOption } from "./NewModelOption";
|
||||
|
||||
const defaultProps = {
|
||||
image: "app/img/sql_illustration",
|
||||
image: { src: "sql_illustration.png", srcSet: "sql_illustration@2x.png 2x" },
|
||||
title: "Use a native query",
|
||||
description: "Fall back to SQL.",
|
||||
to: "/model/new/native",
|
||||
@@ -42,8 +42,8 @@ describe("NewModelOption", () => {
|
||||
setup({ width: 180 });
|
||||
|
||||
const img = screen.getByRole("img");
|
||||
expect(img).toHaveAttribute("src", "app/img/sql_illustration.png");
|
||||
expect(img).toHaveAttribute("srcset", "app/img/sql_illustration@2x.png 2x");
|
||||
expect(img).toHaveAttribute("src", defaultProps.image.src);
|
||||
expect(img).toHaveAttribute("srcset", defaultProps.image.srcSet);
|
||||
expect(img).toHaveAttribute("width", "180");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import cx from "classnames";
|
||||
import { t } from "ttag";
|
||||
|
||||
import notebookModeIllustration from "img/notebook_mode_illustration.png";
|
||||
import notebookModeIllustration2x from "img/notebook_mode_illustration@2x.png";
|
||||
import sqlIllustration from "img/sql_illustration.png";
|
||||
import sqlIllustration2x from "img/sql_illustration@2x.png";
|
||||
import { ExternalLink } from "metabase/common/components/ExternalLink";
|
||||
import { NoDatabasesEmptyState } from "metabase/common/components/NoDatabasesEmptyState";
|
||||
import CS from "metabase/css/core/index.css";
|
||||
@@ -60,7 +64,10 @@ const NewModelOptions = () => {
|
||||
{hasDataAccess && (
|
||||
<div>
|
||||
<NewModelOption
|
||||
image="app/img/notebook_mode_illustration"
|
||||
image={{
|
||||
src: notebookModeIllustration,
|
||||
srcSet: `${notebookModeIllustration2x} 2x`,
|
||||
}}
|
||||
title={t`Use the notebook editor`}
|
||||
description={t`This automatically inherits metadata from your source tables, and gives your models drill-through.`}
|
||||
width={180}
|
||||
@@ -76,7 +83,10 @@ const NewModelOptions = () => {
|
||||
{hasNativeWrite && (
|
||||
<div>
|
||||
<NewModelOption
|
||||
image="app/img/sql_illustration"
|
||||
image={{
|
||||
src: sqlIllustration,
|
||||
srcSet: `${sqlIllustration2x} 2x`,
|
||||
}}
|
||||
title={t`Use a native query`}
|
||||
description={t`You can always fall back to a SQL or native query, which is a bit more manual.`}
|
||||
to={Urls.newQuestion({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import dependencyDiagnosticsUpsellImage from "assets/img/data-studio-dependency-diagnostics-upsell.svg";
|
||||
import { usePageTitle } from "metabase/hooks/use-page-title";
|
||||
import { BaseUpsellPage } from "metabase/monitor/upsells";
|
||||
|
||||
@@ -13,7 +14,7 @@ export function DependencyDiagnosticsUpsellPage() {
|
||||
header={t`Dependency diagnostics`}
|
||||
title={t`Find and fix broken dependencies without hunting them down`}
|
||||
description={t`Detect broken dependencies across datasets and dashboards, and resolve issues without checking everything manually.`}
|
||||
image="app/assets/img/data-studio-dependency-diagnostics-upsell.svg"
|
||||
image={dependencyDiagnosticsUpsellImage}
|
||||
variant="image-full-height"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import upsellPerformanceToolsImage from "assets/img/upsell-performance-tools.png";
|
||||
import { usePageTitle } from "metabase/hooks/use-page-title";
|
||||
import { BaseUpsellPage } from "metabase/monitor/upsells";
|
||||
|
||||
@@ -13,7 +14,7 @@ export const MonitorUpsell = () => {
|
||||
header={t`Erroring questions`}
|
||||
title={t`Troubleshoot faster`}
|
||||
description={t`Find and fix issues fast, with an overview of all errors and model caching logs.`}
|
||||
image="app/assets/img/upsell-performance-tools.png"
|
||||
image={upsellPerformanceToolsImage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ReactNode } from "react";
|
||||
import { match } from "ts-pattern";
|
||||
import { c, t } from "ttag";
|
||||
|
||||
import databasesImage from "assets/img/empty-states/databases.svg?url";
|
||||
import { Link } from "metabase/common/components/Link";
|
||||
import { StoragePurchaseButton } from "metabase/common/components/upsells/StoragePurchaseModal";
|
||||
import { useSelector } from "metabase/redux";
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
Text,
|
||||
Title,
|
||||
} from "metabase/ui";
|
||||
import { getSubpathSafeUrl } from "metabase/urls";
|
||||
|
||||
import IconCSV from "./illustrations/csv.svg?component";
|
||||
|
||||
@@ -160,15 +160,11 @@ const AddDataEmptyState = ({
|
||||
};
|
||||
|
||||
export const DatabasePanelEmptyState = () => {
|
||||
const illustration = getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/databases.svg",
|
||||
);
|
||||
|
||||
return (
|
||||
<AddDataEmptyState
|
||||
title={t`Add a database`}
|
||||
subtitle={t`Start exploring in minutes. We support more than 20 data connectors.`}
|
||||
illustration={<Center component="img" src={illustration} w="3rem" />}
|
||||
illustration={<Center component="img" src={databasesImage} w="3rem" />}
|
||||
contactAdminReason="add-database"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ComponentType, ReactNode } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
import noResultsSource from "assets/img/no_results.svg";
|
||||
import bridgeImage from "img/bridge.svg?url";
|
||||
import type { AdminPathKey, State } from "metabase/redux/store";
|
||||
import type { UiParameter } from "metabase-lib/v1/parameters/types";
|
||||
import type { Dashboard, DraftDashboardSubscription } from "metabase-types/api";
|
||||
@@ -33,12 +34,12 @@ interface PluginDashboardSubscriptionParametersSectionOverride {
|
||||
}
|
||||
|
||||
const defaultLandingPageIllustration = {
|
||||
src: "app/img/bridge.svg",
|
||||
src: bridgeImage,
|
||||
isDefault: true,
|
||||
};
|
||||
|
||||
const defaultLoginPageIllustration = {
|
||||
src: "app/img/bridge.svg",
|
||||
src: bridgeImage,
|
||||
isDefault: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import cx from "classnames";
|
||||
import type { CSSProperties } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
import segmentsListImage from "assets/img/segments-list.png";
|
||||
import segmentsListImage2x from "assets/img/segments-list@2x.png";
|
||||
import { useListSegmentsQuery } from "metabase/api";
|
||||
import { AdminAwareEmptyState } from "metabase/common/components/AdminAwareEmptyState";
|
||||
import { LoadingAndErrorWrapper } from "metabase/common/components/LoadingAndErrorWrapper";
|
||||
@@ -26,7 +28,10 @@ const emptyStateData = {
|
||||
get message() {
|
||||
return t`Segments will appear here once your admins have created some`;
|
||||
},
|
||||
image: "app/assets/img/segments-list",
|
||||
image: {
|
||||
src: segmentsListImage,
|
||||
srcSet: `${segmentsListImage2x} 2x`,
|
||||
},
|
||||
get adminAction() {
|
||||
return t`Learn how to create segments`;
|
||||
},
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import areaImage from "assets/img/empty-states/visualizations/area.svg?url";
|
||||
import barImage from "assets/img/empty-states/visualizations/bar.svg?url";
|
||||
import comboImage from "assets/img/empty-states/visualizations/combo.svg?url";
|
||||
import funnelImage from "assets/img/empty-states/visualizations/funnel.svg?url";
|
||||
import gaugeImage from "assets/img/empty-states/visualizations/gauge.svg?url";
|
||||
import lineImage from "assets/img/empty-states/visualizations/line.svg?url";
|
||||
import mapRegionImage from "assets/img/empty-states/visualizations/map-region.svg?url";
|
||||
import pieImage from "assets/img/empty-states/visualizations/pie.svg?url";
|
||||
import pivotImage from "assets/img/empty-states/visualizations/pivot.svg?url";
|
||||
import progressImage from "assets/img/empty-states/visualizations/progress.svg?url";
|
||||
import rowImage from "assets/img/empty-states/visualizations/row.svg?url";
|
||||
import sankeyImage from "assets/img/empty-states/visualizations/sankey.svg?url";
|
||||
import scalarImage from "assets/img/empty-states/visualizations/scalar.svg?url";
|
||||
import scatterImage from "assets/img/empty-states/visualizations/scatter.svg?url";
|
||||
import smartscalarImage from "assets/img/empty-states/visualizations/smartscalar.svg?url";
|
||||
import treemapImage from "assets/img/empty-states/visualizations/treemap.svg?url";
|
||||
import waterfallImage from "assets/img/empty-states/visualizations/waterfall.svg?url";
|
||||
import { getSubpathSafeUrl } from "metabase/urls";
|
||||
import type { CardDisplayType } from "metabase-types/api";
|
||||
|
||||
@@ -24,16 +41,9 @@ type EmptyVizConfig = {
|
||||
docsLink?: string;
|
||||
};
|
||||
|
||||
// We should not import these large empty-state images directly,
|
||||
// because we don't need to inline them as base64 to the SDK bundle.
|
||||
// Instead we just define paths to them that is passed to the <img> tag directly.
|
||||
const emptyVizConfig: Record<SupportedDisplayType, EmptyVizConfig> = {
|
||||
area: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/area.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: areaImage,
|
||||
get primaryText() {
|
||||
return t`Then pick a metric and multiple columns to group by.`;
|
||||
},
|
||||
@@ -42,11 +52,7 @@ const emptyVizConfig: Record<SupportedDisplayType, EmptyVizConfig> = {
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/bar.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: barImage,
|
||||
get primaryText() {
|
||||
return t`Then pick a metric and a column to group by.`;
|
||||
},
|
||||
@@ -55,11 +61,7 @@ const emptyVizConfig: Record<SupportedDisplayType, EmptyVizConfig> = {
|
||||
},
|
||||
},
|
||||
combo: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/combo.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: comboImage,
|
||||
get primaryText() {
|
||||
return t`Then pick two or more metrics and one or two columns to group by.`;
|
||||
},
|
||||
@@ -68,11 +70,7 @@ const emptyVizConfig: Record<SupportedDisplayType, EmptyVizConfig> = {
|
||||
},
|
||||
},
|
||||
funnel: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/funnel.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: funnelImage,
|
||||
get primaryText() {
|
||||
return t`Funnel charts visualize how a value is broken out by a series of steps, and the percent change between steps.`;
|
||||
},
|
||||
@@ -82,11 +80,7 @@ const emptyVizConfig: Record<SupportedDisplayType, EmptyVizConfig> = {
|
||||
docsLink: getSubpathSafeUrl("questions/visualizations/funnel"),
|
||||
},
|
||||
gauge: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/gauge.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: gaugeImage,
|
||||
get primaryText() {
|
||||
return t`Then pick an aggregate metric (such as Average or Sum) and customize the gauge in the visualization settings.`;
|
||||
},
|
||||
@@ -95,11 +89,7 @@ const emptyVizConfig: Record<SupportedDisplayType, EmptyVizConfig> = {
|
||||
},
|
||||
},
|
||||
line: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/line.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: lineImage,
|
||||
get primaryText() {
|
||||
return t`Then pick one or more metrics and a time column to group by.`;
|
||||
},
|
||||
@@ -108,11 +98,7 @@ const emptyVizConfig: Record<SupportedDisplayType, EmptyVizConfig> = {
|
||||
},
|
||||
},
|
||||
map: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/map-region.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: mapRegionImage,
|
||||
get primaryText() {
|
||||
return t`Build map visualizations with geospatial data: Pin and Grid maps require longitude and latitude columns, Region maps require a column with region names.`;
|
||||
},
|
||||
@@ -122,11 +108,7 @@ const emptyVizConfig: Record<SupportedDisplayType, EmptyVizConfig> = {
|
||||
docsLink: "questions/visualizations/map",
|
||||
},
|
||||
pie: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/pie.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: pieImage,
|
||||
get primaryText() {
|
||||
return t`Then pick a metric and a column to group by.`;
|
||||
},
|
||||
@@ -135,11 +117,7 @@ const emptyVizConfig: Record<SupportedDisplayType, EmptyVizConfig> = {
|
||||
},
|
||||
},
|
||||
pivot: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/pivot.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: pivotImage,
|
||||
get primaryText() {
|
||||
return t`Then pick an aggregate metric (such as Average or Sum) and multiple columns to group by.`;
|
||||
},
|
||||
@@ -148,11 +126,7 @@ const emptyVizConfig: Record<SupportedDisplayType, EmptyVizConfig> = {
|
||||
},
|
||||
},
|
||||
progress: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/progress.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: progressImage,
|
||||
get primaryText() {
|
||||
return t`Then pick an aggregate metric (such as Count or Sum) and customize the progress bar in the visualization settings.`;
|
||||
},
|
||||
@@ -161,11 +135,7 @@ const emptyVizConfig: Record<SupportedDisplayType, EmptyVizConfig> = {
|
||||
},
|
||||
},
|
||||
row: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/row.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: rowImage,
|
||||
get primaryText() {
|
||||
return t`Then pick a metric and a column to group by.`;
|
||||
},
|
||||
@@ -174,11 +144,7 @@ const emptyVizConfig: Record<SupportedDisplayType, EmptyVizConfig> = {
|
||||
},
|
||||
},
|
||||
sankey: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/sankey.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: sankeyImage,
|
||||
get primaryText() {
|
||||
return t`Sankey charts show how data flows through multi-dimensional steps. They're useful for showing which elements, called nodes, contribute to the overall flow.`;
|
||||
},
|
||||
@@ -188,22 +154,14 @@ const emptyVizConfig: Record<SupportedDisplayType, EmptyVizConfig> = {
|
||||
docsLink: "questions/visualizations/sankey",
|
||||
},
|
||||
scalar: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/scalar.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: scalarImage,
|
||||
get primaryText() {
|
||||
return t`Then pick an aggregate metric (such as Average or Sum).`;
|
||||
},
|
||||
secondaryText: `E.g. Average star rating`,
|
||||
},
|
||||
scatter: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/scatter.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: scatterImage,
|
||||
get primaryText() {
|
||||
return t`Then pick one or more metrics and a column to group by.`;
|
||||
},
|
||||
@@ -212,11 +170,7 @@ const emptyVizConfig: Record<SupportedDisplayType, EmptyVizConfig> = {
|
||||
},
|
||||
},
|
||||
smartscalar: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/smartscalar.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: smartscalarImage,
|
||||
get primaryText() {
|
||||
return t`Then pick an aggregate metric (such as the Average or Sum) and a time column to group by.`;
|
||||
},
|
||||
@@ -225,11 +179,7 @@ const emptyVizConfig: Record<SupportedDisplayType, EmptyVizConfig> = {
|
||||
},
|
||||
},
|
||||
treemap: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/treemap.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: treemapImage,
|
||||
get primaryText() {
|
||||
return t`Then pick a metric and one or two columns to group by.`;
|
||||
},
|
||||
@@ -238,11 +188,7 @@ const emptyVizConfig: Record<SupportedDisplayType, EmptyVizConfig> = {
|
||||
},
|
||||
},
|
||||
waterfall: {
|
||||
get imgSrc() {
|
||||
return getSubpathSafeUrl(
|
||||
"app/assets/img/empty-states/visualizations/waterfall.svg",
|
||||
);
|
||||
},
|
||||
imgSrc: waterfallImage,
|
||||
get primaryText() {
|
||||
return t`Then pick a metric and a single column to group by: either time or category.`;
|
||||
},
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import mapImage from "assets/img/map.svg?url";
|
||||
import { ChartSkeletonImage } from "metabase/visualizations/components/skeletons/ChartSkeleton/ChartSkeletonImage";
|
||||
|
||||
const MapSkeleton = (): JSX.Element => {
|
||||
return (
|
||||
<ChartSkeletonImage viewBox="0 0 242 157" preserveAspectRatio="xMidYMid">
|
||||
<use xlinkHref="/app/assets/img/map.svg" />
|
||||
<use xlinkHref={mapImage} />
|
||||
</ChartSkeletonImage>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,9 +3,10 @@ import { useCallback, useMemo } from "react";
|
||||
import { useLatest } from "react-use";
|
||||
import { t } from "ttag";
|
||||
|
||||
import hiddenFieldImage from "assets/img/hidden-field.png?url";
|
||||
import hiddenFieldImage2x from "assets/img/hidden-field@2x.png?url";
|
||||
import CS from "metabase/css/core/index.css";
|
||||
import { useQuestionFromCardBuilder } from "metabase/metadata-store";
|
||||
import { getSubpathSafeUrl } from "metabase/urls";
|
||||
import {
|
||||
type VisibleTableData,
|
||||
getVisibleTableData,
|
||||
@@ -97,13 +98,6 @@ function useSyncedQuestion(series: VisualizationProps["series"]) {
|
||||
}
|
||||
|
||||
function AllFieldsHiddenMessage({ isDashboard }: { isDashboard: boolean }) {
|
||||
const allFieldsHiddenImageUrl = getSubpathSafeUrl(
|
||||
"app/assets/img/hidden-field.png",
|
||||
);
|
||||
const allFieldsHiddenImage2xUrl = getSubpathSafeUrl(
|
||||
"app/assets/img/hidden-field@2x.png",
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
@@ -120,10 +114,10 @@ function AllFieldsHiddenMessage({ isDashboard }: { isDashboard: boolean }) {
|
||||
<img
|
||||
data-testid="Table-all-fields-hidden-image"
|
||||
width={99}
|
||||
src={allFieldsHiddenImageUrl}
|
||||
src={hiddenFieldImage}
|
||||
srcSet={`
|
||||
${allFieldsHiddenImageUrl} 1x,
|
||||
${allFieldsHiddenImage2xUrl} 2x
|
||||
${hiddenFieldImage} 1x,
|
||||
${hiddenFieldImage2x} 2x
|
||||
`}
|
||||
className={CS.mb2}
|
||||
/>
|
||||
|
||||
20
frontend/src/types/import-image.d.ts
vendored
Normal file
20
frontend/src/types/import-image.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
declare module "*.png" {
|
||||
const value: string;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "*.png?url" {
|
||||
const value: string;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "*.gif" {
|
||||
const value: string;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default value;
|
||||
}
|
||||
7
frontend/src/types/import-svg.d.ts
vendored
7
frontend/src/types/import-svg.d.ts
vendored
@@ -5,6 +5,13 @@ declare module "*.svg" {
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "*.svg?url" {
|
||||
const value: string;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "*.svg?source" {
|
||||
const value: string;
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ const baseConfig = {
|
||||
"^remend$": "<rootDir>/node_modules/remend/dist/index.js",
|
||||
"^build-configs/(.*)$": "<rootDir>/frontend/build/$1",
|
||||
"\\.(css|less)$": "<rootDir>/frontend/test/__mocks__/styleMock.js",
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)(\\?url)?$":
|
||||
"<rootDir>/frontend/test/__mocks__/fileMock.js",
|
||||
"^cljs/(.*)$": "<rootDir>/target/cljs_dev/$1",
|
||||
// The real catalogues are a build artifact, so tests resolve a small fixture
|
||||
|
||||
@@ -8,7 +8,7 @@ const esmPackages = require("./jest.esm-packages.js");
|
||||
module.exports = {
|
||||
moduleNameMapper: {
|
||||
"\\.(css|less)$": "<rootDir>/frontend/test/__mocks__/styleMock.js",
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)(\\?url)?$":
|
||||
"<rootDir>/frontend/test/__mocks__/fileMock.js",
|
||||
"^cljs/(.*)$": "<rootDir>/target/cljs_dev/$1",
|
||||
"\\.svg\\?(component|source)":
|
||||
|
||||
@@ -134,7 +134,19 @@ const config = {
|
||||
{
|
||||
test: /\.(svg|png)$/,
|
||||
type: "asset/inline",
|
||||
resourceQuery: { not: [/component|source/] },
|
||||
resourceQuery: { not: [/component|source|url/] },
|
||||
},
|
||||
{
|
||||
// `?url` opts an image out of inlining. The app build owns the emitted
|
||||
// file, this build only needs its URL: `../dist/` resolves from the
|
||||
// SDK's publicPath (`app/embedding-sdk/`) to the app's `app/dist/`.
|
||||
test: /\.(svg|png)$/,
|
||||
resourceQuery: /url/,
|
||||
type: "asset/resource",
|
||||
generator: {
|
||||
emit: false,
|
||||
filename: "../dist/[hash][ext]",
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
|
||||
@@ -238,6 +238,9 @@ const config = {
|
||||
test: /\.(svg|png)$/,
|
||||
type: "asset/resource",
|
||||
resourceQuery: { not: [/component|source/] },
|
||||
// No `[query]`: a `?url` import must emit the same name as the SDK
|
||||
// build references.
|
||||
generator: { filename: "[hash][ext]" },
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
|
||||
Reference in New Issue
Block a user