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:
Romeo Van Snick
2026-10-07 10:25:24 +11:00
committed by GitHub
parent 1c0661fa03
commit 3d9a6743c6
55 changed files with 273 additions and 216 deletions

View File

@@ -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}
/>
);
}

View File

@@ -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}
/>
);
}

View File

@@ -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}
/>
);
}

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,
};

View File

@@ -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`;

View File

@@ -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 () => {

View File

@@ -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.`}{" "}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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.`}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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"];
};

View File

@@ -1 +1 @@
export { EmptyState } from "./EmptyState";
export { EmptyState, type ImageSource } from "./EmptyState";

View File

@@ -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"
/>

View File

@@ -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<

View File

@@ -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 = {

View File

@@ -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;

View File

@@ -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) => {

View File

@@ -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}
/>
);
}

View File

@@ -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"
/>
);

View File

@@ -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"

View File

@@ -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}
/>
);
}

View File

@@ -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 = [

View File

@@ -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>) => {

View File

@@ -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}
/>

View File

@@ -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"
/>
)}

View File

@@ -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"
/>
)}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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.`}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}>

View File

@@ -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");
});

View File

@@ -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({

View File

@@ -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"
/>
);

View File

@@ -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}
/>
);
};

View File

@@ -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"
/>
);

View File

@@ -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,
};

View File

@@ -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`;
},

View File

@@ -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.`;
},

View File

@@ -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>
);
};

View File

@@ -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
View 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;
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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)":

View File

@@ -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$/,

View File

@@ -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$/,