diff --git a/frontend/src/metabase/browse/components/BrowseModels.unit.spec.tsx b/frontend/src/metabase/browse/components/BrowseModels.unit.spec.tsx index 1d059fc2f9d1082022f891c58e95caf15b1e8fc7..e05f459ff2e5147c0f6b411fd317ef5667de8f08 100644 --- a/frontend/src/metabase/browse/components/BrowseModels.unit.spec.tsx +++ b/frontend/src/metabase/browse/components/BrowseModels.unit.spec.tsx @@ -4,7 +4,6 @@ import { setupSettingsEndpoints, } from "__support__/server-mocks"; import { renderWithProviders, screen, within } from "__support__/ui"; -import { defaultRootCollection } from "metabase/admin/permissions/pages/CollectionPermissionsPage/tests/setup"; import { createMockCollection, createMockSearchResult, @@ -15,6 +14,11 @@ import { createMockModelResult, createMockRecentModel } from "../test-utils"; import { BrowseModels } from "./BrowseModels"; +const defaultRootCollection = createMockCollection({ + id: "root", + name: "Our analytics", +}); + const setup = (modelCount: number, recentModelCount = 5) => { const mockModelResults = mockModels.map(model => createMockModelResult(model), @@ -249,14 +253,14 @@ const mockModels = [ { id: 21, name: "Model 21", - collection: defaultRootCollection, + collection: defaultRootCollection, // Our analytics last_editor_common_name: "Bobby", last_edited_at: "2000-01-01T00:00:00.000Z", }, { id: 22, name: "Model 22", - collection: defaultRootCollection, + collection: defaultRootCollection, // Our analytics last_editor_common_name: "Bobby", last_edited_at: "2000-01-01T00:00:00.000Z", }, @@ -284,7 +288,9 @@ describe("BrowseModels", () => { }); expect(modelsTable).toBeInTheDocument(); expect( - await screen.findAllByTestId("path-for-collection: Our analytics"), + await within(modelsTable).findAllByTestId( + "path-for-collection: Our analytics", + ), ).toHaveLength(2); expect( await within(modelsTable).findByText("Model 20"), @@ -304,9 +310,7 @@ describe("BrowseModels", () => { }); expect(await within(modelsTable).findByText("Model 1")).toBeInTheDocument(); expect( - await within(modelsTable).findAllByTestId( - "breadcrumbs-for-collection: Alpha", - ), + await within(modelsTable).findAllByTestId("path-for-collection: Alpha"), ).toHaveLength(3); }); diff --git a/frontend/src/metabase/browse/components/CollectionBreadcrumbsWithTooltip.styled.tsx b/frontend/src/metabase/browse/components/CollectionBreadcrumbsWithTooltip.styled.tsx deleted file mode 100644 index 22d5944a987a905da89b1d97af4bf4cf7217511f..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/browse/components/CollectionBreadcrumbsWithTooltip.styled.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import styled from "@emotion/styled"; - -import { ResponsiveChild } from "metabase/components/ResponsiveContainer/ResponsiveContainer"; -import Link from "metabase/core/components/Link"; -import { FixedSizeIcon, Flex, Group } from "metabase/ui"; - -import { Ellipsis } from "./Ellipsis"; - -/** When a cell is narrower than this width, breadcrumbs within it change significantly */ -const breadcrumbBreakpoint = "10rem"; - -export const Breadcrumb = styled(ResponsiveChild)<{ - maxWidth: string; - isSoleBreadcrumb: boolean; - index: number; -}>` - ${({ maxWidth }) => { - return maxWidth ? `td & { max-width: ${maxWidth} };` : ""; - }} - color: var(--mb-color-text-dark); - line-height: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - padding-top: 1px; - padding-bottom: 1px; - ${props => { - return ` - @container ${props.containerName} (width < ${breadcrumbBreakpoint}) { - ${props.index === 0 && !props.isSoleBreadcrumb ? `display: none;` : ""} - td & { max-width: calc(95cqw - ${props.isSoleBreadcrumb ? 1 : 3}rem); }; - }`; - }} -`; - -export const CollectionLink = styled(Link)` - :hover { - &, - * { - color: var(--mb-color-brand); - - .collection-path-separator { - color: var(--mb-color-brand-alpha-88); - } - } - } -`; - -export const InitialEllipsis = styled(Ellipsis)``; -InitialEllipsis.defaultProps = { - includeSep: false, -}; - -export const CollectionBreadcrumbsWrapper = styled(ResponsiveChild)` - line-height: 1; - ${InitialEllipsis} { - display: none; - } - ${props => { - return ` - @container ${props.containerName} (width < ${breadcrumbBreakpoint}) { - ${EllipsisAndSeparator} { - display: none; - } - ${InitialEllipsis} { - display: inline; - } - } - `; - }} -`; - -export const BreadcrumbGroup = styled(Group)` - flex-flow: row nowrap; -`; - -export const CollectionsIcon = styled(FixedSizeIcon)` - margin-inline-end: 0.5rem; -`; - -export const EllipsisAndSeparator = styled(Flex)``; diff --git a/frontend/src/metabase/browse/components/CollectionBreadcrumbsWithTooltip.tsx b/frontend/src/metabase/browse/components/CollectionBreadcrumbsWithTooltip.tsx deleted file mode 100644 index a96085dd695b25b0f11430b3762d498f3b5c5313..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/browse/components/CollectionBreadcrumbsWithTooltip.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { useEffect, useRef, useState } from "react"; - -import { getCollectionName } from "metabase/collections/utils"; -import { ResponsiveContainer } from "metabase/components/ResponsiveContainer/ResponsiveContainer"; -import { Ellipsified } from "metabase/core/components/Ellipsified"; -import { useAreAnyTruncated } from "metabase/hooks/use-is-truncated"; -import resizeObserver from "metabase/lib/resize-observer"; -import * as Urls from "metabase/lib/urls"; -import { Flex, Tooltip } from "metabase/ui"; -import type { CollectionEssentials } from "metabase-types/api"; - -import { - Breadcrumb, - BreadcrumbGroup, - CollectionBreadcrumbsWrapper, - CollectionLink, - CollectionsIcon, - InitialEllipsis, -} from "./CollectionBreadcrumbsWithTooltip.styled"; -import { Ellipsis } from "./Ellipsis"; -import { PathSeparator } from "./PathSeparator"; -import { getBreadcrumbMaxWidths, getCollectionPathString } from "./utils"; - -export const CollectionBreadcrumbsWithTooltip = ({ - collection, - containerName, -}: { - collection: CollectionEssentials; - containerName: string; -}) => { - const collections = ( - (collection.effective_ancestors as CollectionEssentials[]) || [] - ).concat(collection); - const pathString = getCollectionPathString(collection); - const ellipsifyPath = collections.length > 2; - const shownCollections = ellipsifyPath - ? [collections[0], collections[collections.length - 1]] - : collections; - const justOneShown = shownCollections.length === 1; - - const { areAnyTruncated, ref } = useAreAnyTruncated<HTMLDivElement>(); - - const initialEllipsisRef = useRef<HTMLDivElement | null>(null); - const [ - isFirstCollectionDisplayedAsEllipsis, - setIsFirstCollectionDisplayedAsEllipsis, - ] = useState(false); - - useEffect(() => { - const initialEllipsis = initialEllipsisRef.current; - if (!initialEllipsis) { - return; - } - const handleResize = () => { - // The initial ellipsis might be hidden via CSS, - // so we need to check whether it is displayed via getComputedStyle - const style = window.getComputedStyle(initialEllipsis); - setIsFirstCollectionDisplayedAsEllipsis(style.display !== "none"); - }; - resizeObserver.subscribe(initialEllipsis, handleResize); - return () => { - resizeObserver.unsubscribe(initialEllipsis, handleResize); - }; - }, [initialEllipsisRef]); - - const isTooltipEnabled = - areAnyTruncated || ellipsifyPath || isFirstCollectionDisplayedAsEllipsis; - - const maxWidths = getBreadcrumbMaxWidths(shownCollections, 96, ellipsifyPath); - - return ( - <Tooltip - label={pathString} - disabled={!isTooltipEnabled} - multiline - maw="20rem" - > - <ResponsiveContainer - aria-label={pathString} - data-testid={`breadcrumbs-for-collection: ${collection.name}`} - name={containerName} - w="auto" - > - <CollectionLink to={Urls.collection(collection)}> - <Flex - align="center" - w="100%" - lh="1" - style={{ flexFlow: "row nowrap" }} - > - <CollectionsIcon - name="folder" - // Stopping propagation so that the parent <tr>'s onclick won't fire - onClick={(e: React.MouseEvent) => e.stopPropagation()} - /> - {shownCollections.map((collection, index) => { - const key = `collection${collection.id}`; - return ( - <BreadcrumbGroup - spacing={0} - key={key} - // Stopping propagation so that the parent <tr>'s onclick won't fire - onClick={(e: React.MouseEvent) => e.stopPropagation()} - > - {index > 0 && <PathSeparator />} - <CollectionBreadcrumbsWrapper - containerName={containerName} - style={{ alignItems: "center" }} - w="auto" - display="flex" - > - {index === 0 && !justOneShown && ( - <InitialEllipsis ref={initialEllipsisRef} /> - )} - {index > 0 && ellipsifyPath && <Ellipsis />} - <Breadcrumb - maxWidth={maxWidths[index]} - index={index} - isSoleBreadcrumb={collections.length === 1} - containerName={containerName} - ref={(el: HTMLDivElement | null) => - el && ref.current.set(key, el) - } - key={collection.id} - > - {getCollectionName(collection)} - </Breadcrumb> - </CollectionBreadcrumbsWrapper> - </BreadcrumbGroup> - ); - })} - </Flex> - </CollectionLink> - </ResponsiveContainer> - </Tooltip> - ); -}; - -export const SimpleCollectionDisplay = ({ - collection, -}: { - collection: CollectionEssentials; -}) => { - return ( - <Flex align="center"> - <CollectionsIcon name="folder" /> - <Ellipsified>{getCollectionName(collection)}</Ellipsified> - </Flex> - ); -}; diff --git a/frontend/src/metabase/browse/components/CollectionBreadcrumbsWithTooltip.unit.spec.tsx b/frontend/src/metabase/browse/components/CollectionBreadcrumbsWithTooltip.unit.spec.tsx deleted file mode 100644 index e48aa72861c790d0f4f95ada45a85654a418bf83..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/browse/components/CollectionBreadcrumbsWithTooltip.unit.spec.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { renderWithProviders, screen } from "__support__/ui"; -import type { Collection } from "metabase-types/api"; -import { createMockCollection } from "metabase-types/api/mocks"; - -import { CollectionBreadcrumbsWithTooltip } from "./CollectionBreadcrumbsWithTooltip"; - -const setup = (collection: Collection) => { - return renderWithProviders( - <CollectionBreadcrumbsWithTooltip - collection={collection} - containerName="Container" - />, - ); -}; -const collectionAlpha = createMockCollection({ id: 99, name: "Alpha" }); -const collectionBeta = createMockCollection({ - id: 1, - name: "Beta", - effective_ancestors: [collectionAlpha], -}); -const collectionCharlie = createMockCollection({ - id: 2, - name: "Charlie", - effective_ancestors: [collectionAlpha, collectionBeta], -}); - -describe("CollectionBreadcrumbsWithTooltip", () => { - it("should show a single collection", () => { - setup(collectionAlpha); - expect(screen.getByLabelText("Alpha")).toBeInTheDocument(); - expect(screen.getByText("Alpha")).toBeInTheDocument(); - }); - - it("should display a path of length two without abbreviations", () => { - setup(collectionBeta); - const breadcrumbs = screen.getByLabelText("Alpha / Beta"); - expect(breadcrumbs).toBeInTheDocument(); - expect(breadcrumbs?.textContent).toMatch(/Alpha\/Beta/); - }); - - it("should display a path of length three as the first and last collection with an ellipsis in between", () => { - setup(collectionCharlie); - const breadcrumbs = screen.getByLabelText("Alpha / Beta / Charlie"); - expect(breadcrumbs).toBeInTheDocument(); - expect(breadcrumbs?.textContent).toMatch(/Alpha\/…\/Charlie/); - }); -}); diff --git a/frontend/src/metabase/browse/components/Ellipsis.tsx b/frontend/src/metabase/browse/components/Ellipsis.tsx deleted file mode 100644 index e748d098625a8f2aad4ee218b6f5e596b79c4d32..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/browse/components/Ellipsis.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import type { FC } from "react"; -import { forwardRef } from "react"; - -import type { FlexProps } from "metabase/ui"; -import { Text } from "metabase/ui"; - -import type { RefProp } from "../types"; - -import { EllipsisAndSeparator } from "./CollectionBreadcrumbsWithTooltip.styled"; -import { PathSeparator } from "./PathSeparator"; -type EllipsisProps = { - includeSep?: boolean; -} & FlexProps; - -export const Ellipsis: FC< - EllipsisProps & Partial<RefProp<HTMLDivElement | null>> -> = forwardRef<HTMLDivElement, EllipsisProps>( - ({ includeSep = true, ...flexProps }, ref) => ( - <EllipsisAndSeparator - ref={ref} - align="center" - className="ellipsis-and-separator" - {...flexProps} - > - <Text lh={1}>…</Text> - {includeSep && <PathSeparator />} - </EllipsisAndSeparator> - ), -); -Ellipsis.displayName = "Ellipsis"; diff --git a/frontend/src/metabase/browse/components/ModelsTable.module.css b/frontend/src/metabase/browse/components/ModelsTable.module.css new file mode 100644 index 0000000000000000000000000000000000000000..f29119d547afe2c2cd394305494d1847877b0312 --- /dev/null +++ b/frontend/src/metabase/browse/components/ModelsTable.module.css @@ -0,0 +1,5 @@ +.collectionLink { + :hover { + color: var(--mb-color-brand) !important; + } +} diff --git a/frontend/src/metabase/browse/components/ModelsTable.tsx b/frontend/src/metabase/browse/components/ModelsTable.tsx index b1a211dd8f34b5fb9ac233649c85945854b59bc5..0f508b326c65416dd5607182942142aa666a22ab 100644 --- a/frontend/src/metabase/browse/components/ModelsTable.tsx +++ b/frontend/src/metabase/browse/components/ModelsTable.tsx @@ -1,13 +1,9 @@ -import { - type PropsWithChildren, - useEffect, - useState, - type CSSProperties, -} from "react"; +import { type PropsWithChildren, useState, type CSSProperties } from "react"; import { push } from "react-router-redux"; import { t } from "ttag"; import { getCollectionName } from "metabase/collections/utils"; +import { EllipsifiedPath } from "metabase/common/components/EllipsifiedPath"; import { useLocale } from "metabase/common/hooks/use-locale/use-locale"; import EntityItem from "metabase/components/EntityItem"; import { SortableColumnHeader } from "metabase/components/ItemsTable/BaseItemsTable"; @@ -21,7 +17,7 @@ import { import { Columns } from "metabase/components/ItemsTable/Columns"; import type { ResponsiveProps } from "metabase/components/ItemsTable/utils"; import { Ellipsified } from "metabase/core/components/Ellipsified"; -import { color } from "metabase/lib/colors"; +import Link from "metabase/core/components/Link"; import { useDispatch } from "metabase/lib/redux"; import * as Urls from "metabase/lib/urls"; import { @@ -30,6 +26,8 @@ import { type IconProps, type IconName, Skeleton, + FixedSizeIcon, + Box, } from "metabase/ui"; import { Repeat } from "metabase/ui/components/feedback/Skeleton/Repeat"; import { SortDirection, type SortingOptions } from "metabase-types/api/sorting"; @@ -38,18 +36,18 @@ import { trackModelClick } from "../analytics"; import type { ModelResult } from "../types"; import { getIcon } from "../utils"; -import { - CollectionBreadcrumbsWithTooltip, - SimpleCollectionDisplay, -} from "./CollectionBreadcrumbsWithTooltip"; -import { CollectionsIcon } from "./CollectionBreadcrumbsWithTooltip.styled"; import { EllipsifiedWithMarkdownTooltip } from "./EllipsifiedWithMarkdownTooltip"; +import S from "./ModelsTable.module.css"; import { ModelCell, ModelNameColumn, ModelTableRow, } from "./ModelsTable.styled"; -import { getModelDescription, sortModels } from "./utils"; +import { + getCollectionPathString, + getModelDescription, + sortModels, +} from "./utils"; export interface ModelsTableProps { models?: ModelResult[]; @@ -74,18 +72,10 @@ const DEFAULT_SORTING_OPTIONS: SortingOptions = { sort_direction: SortDirection.Asc, }; -const LARGE_DATASET_THRESHOLD = 500; - export const ModelsTable = ({ models = [], skeleton = false, }: ModelsTableProps) => { - // for large datasets, we need to simplify the display to avoid performance issues - const isLargeDataset = models.length > LARGE_DATASET_THRESHOLD; - - const [showLoadingManyRows, setShowLoadingManyRows] = - useState(isLargeDataset); - const [sortingOptions, setSortingOptions] = useState<SortingOptions>( DEFAULT_SORTING_OPTIONS, ); @@ -100,20 +90,9 @@ export const ModelsTable = ({ const handleUpdateSortOptions = skeleton ? undefined : (newSortingOptions: SortingOptions) => { - if (isLargeDataset) { - setShowLoadingManyRows(true); - } setSortingOptions(newSortingOptions); }; - useEffect(() => { - // we need a better virtualized table solution for large datasets - // for now, we show loading text to make this component feel more responsive - if (isLargeDataset && showLoadingManyRows) { - setTimeout(() => setShowLoadingManyRows(false), 10); - } - }, [isLargeDataset, showLoadingManyRows, sortedModels]); - return ( <Table aria-label={skeleton ? undefined : t`Table of models`}> <colgroup> @@ -169,19 +148,13 @@ export const ModelsTable = ({ </tr> </thead> <TBody> - {showLoadingManyRows ? ( - <TableLoader /> - ) : skeleton ? ( + {skeleton ? ( <Repeat times={7}> <TBodyRowSkeleton /> </Repeat> ) : ( sortedModels.map((model: ModelResult) => ( - <TBodyRow - model={model} - key={`${model.model}-${model.id}`} - simpleDisplay={isLargeDataset} - /> + <TBodyRow model={model} key={`${model.model}-${model.id}`} /> )) )} </TBody> @@ -191,15 +164,12 @@ export const ModelsTable = ({ const TBodyRow = ({ model, - simpleDisplay, skeleton, }: { model: ModelResult; - simpleDisplay: boolean; skeleton?: boolean; }) => { const icon = getIcon(model); - const containerName = `collections-path-for-${model.id}`; const dispatch = useDispatch(); const { id, name } = model; @@ -242,14 +212,24 @@ const TBodyRow = ({ }`} {...collectionProps} > - {simpleDisplay ? ( - <SimpleCollectionDisplay collection={model.collection} /> - ) : ( - <CollectionBreadcrumbsWithTooltip - containerName={containerName} - collection={model.collection} - /> - )} + <Link + className={S.collectionLink} + to={Urls.collection(model.collection)} + onClick={e => e.stopPropagation()} + > + <Flex gap="sm"> + <FixedSizeIcon name="folder" /> + <Box w="calc(100% - 1.5rem)"> + <EllipsifiedPath + tooltip={getCollectionPathString(model.collection)} + items={[ + ...(model.collection?.effective_ancestors || []), + model.collection, + ].map(c => getCollectionName(c))} + /> + </Box> + </Flex> + </Link> </ModelCell> {/* Description */} @@ -295,7 +275,7 @@ const NameCell = ({ <Icon size={16} {...icon} - color={color("brand")} + color={"var(--mb-color-brand)"} style={{ flexShrink: 0 }} /> {children || ( @@ -310,16 +290,6 @@ const NameCell = ({ ); }; -const TableLoader = () => ( - <tr> - <td colSpan={4}> - <Flex justify="center" color="text-light"> - {t`Loading…`} - </Flex> - </td> - </tr> -); - const CellTextSkeleton = () => { return <Skeleton natural h="16.8px" />; }; @@ -335,8 +305,8 @@ const TBodyRowSkeleton = ({ style }: { style?: CSSProperties }) => { {/* Collection */} <ModelCell {...collectionProps}> - <Flex> - <CollectionsIcon name="folder" /> + <Flex gap=".5rem"> + <FixedSizeIcon name="folder" /> <CellTextSkeleton /> </Flex> </ModelCell> diff --git a/frontend/src/metabase/browse/components/PathSeparator.tsx b/frontend/src/metabase/browse/components/PathSeparator.tsx deleted file mode 100644 index dba1db2e93a08baf35cd664c0e357902ec03ccc8..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/browse/components/PathSeparator.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Text } from "metabase/ui"; - -import { pathSeparatorChar } from "./constants"; - -export const PathSeparator = () => ( - <Text - className="collection-path-separator" - color="text-light" - mx=".2rem" - py={1} - > - {pathSeparatorChar} - </Text> -); diff --git a/frontend/src/metabase/common/components/EllipsifiedPath/EllipsifiedPath.module.css b/frontend/src/metabase/common/components/EllipsifiedPath/EllipsifiedPath.module.css new file mode 100644 index 0000000000000000000000000000000000000000..98d405b1e7068d680691eb7ce81e1f913488a349 --- /dev/null +++ b/frontend/src/metabase/common/components/EllipsifiedPath/EllipsifiedPath.module.css @@ -0,0 +1,51 @@ +.path { + flex-grow: 1; + display: flex; + flex-flow: row nowrap; + overflow-x: hidden; + container-type: inline-size; +} + +.slash { + opacity: 0.5; + min-width: 0; + flex-shrink: 0; + padding-inline: 0.1rem; +} + +.item, +.slash { + @container (max-width: 6rem) { + &:not(&:last-child) { + display: none; + } + } +} + +.dots { + opacity: 0.5; + display: none; + + @container (max-width: 6rem) { + display: flex; + } +} + +.item { + flex-grow: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: fit-content; + + /* The first path item should try to be a bit bigger than average */ + &:nth-child(2) { + flex-basis: 100%; + } + + /* The last path item should show all of itself if possible */ + &:last-child { + flex-basis: 1000%; + } +} diff --git a/frontend/src/metabase/common/components/EllipsifiedPath/EllipsifiedPath.tsx b/frontend/src/metabase/common/components/EllipsifiedPath/EllipsifiedPath.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2c59b7236b256802bdd6115d65016a4cf0fc486f --- /dev/null +++ b/frontend/src/metabase/common/components/EllipsifiedPath/EllipsifiedPath.tsx @@ -0,0 +1,47 @@ +import React from "react"; + +import { useAreAnyTruncated } from "metabase/hooks/use-is-truncated"; +import { Tooltip } from "metabase/ui"; + +import S from "./EllipsifiedPath.module.css"; + +type EllipsifiedPathProps = { items: string[]; tooltip: string }; + +/** + * Displays a path such as "Collection / Subcollection / Subsubcollection / + * Parent Collection". + * + * If the path is too long to fit, some items may be truncated, like this: + * "Collection / Subcollec... / Subsub... / Parent Collection". + * + * A tooltip is shown if any items are truncated. + */ +export const EllipsifiedPath = ({ items, tooltip }: EllipsifiedPathProps) => { + const { areAnyTruncated, ref } = useAreAnyTruncated<HTMLDivElement>(); + + return ( + <Tooltip label={tooltip} disabled={!areAnyTruncated} multiline maw="20rem"> + <div className={S.path}> + {items.length > 1 && ( + <div className={S.dots}> + … <div className={S.slash}>/</div> + </div> + )} + {items.map((item, index) => { + const key = `${item}${index}`; + return ( + <React.Fragment key={key}> + <div + ref={el => el && ref.current.set(key, el)} + className={S.item} + > + {item} + </div> + {index < items.length - 1 && <div className={S.slash}>/</div>} + </React.Fragment> + ); + })} + </div> + </Tooltip> + ); +}; diff --git a/frontend/src/metabase/common/components/EllipsifiedPath/index.ts b/frontend/src/metabase/common/components/EllipsifiedPath/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..29d445092bafff7756adc32d57a8e5aab45cdaa3 --- /dev/null +++ b/frontend/src/metabase/common/components/EllipsifiedPath/index.ts @@ -0,0 +1 @@ +export { EllipsifiedPath } from "./EllipsifiedPath";