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

Cherry pick new data picker component to master (#27174)


* Add basic raw data picker for app scaffolding (#25997)

* Show data app page ID params in editing mode (#25943)

* Data apps layout updates (#25930)

* play with grid sizes to make apps feel appier [ci skip]

* move app nav to bar inside app

* only hide other header buttons outside of edit mode

* tweak overflow issue

* fix header width on data apps

* add control to see app contents

* set data apps layout constants contextually [ci skip]

* remove hardcoded value [ci skip]

* extract conextual header styles [ci skip]

* set max-width on paramaters for data apps [ci skip]

* move data apps nav deeper, only show if not editing [ci skip]

* add spacing to contents trigger, rm old code [ci skip]

* rm table thing for now [ci skip]

* Fixes for data app layout updates (#25962)

* Reorder import

* Fix type

* Fix missing translation, move out URL

* Remove not used import

* Rework `getSelectedItems` utility for data apps

* Fix selected data app page highlighting

* Don't reload app navbar on page change

* Change nav item links look

* Remove `DataAppPageSidebarLink`

* Turn nav into HTML list

* Use styled components

* Fix edit button covered by visualizations

* Fix opening homepage

* Remove redundant prop

Co-authored-by: default avatarAnton Kulyk <kuliks.anton@gmail.com>

* First take at parameterized data app page titles (#25938)

* Bring `DataAppContext` back

* Pass `onFocus` and `onBlur` to `EditableText`

* Export `EditableTextProps`

* Add basic `DataAppPageTitle` component

* Use `DataAppPageTitle`

* Fix component name

* Add primitive autocompletion to page title input

* Add `title_template` to `DataAppNavItem` type

* Tweak value management in `DataAppPageTitle`

* Handle `null` values in titles more gracefully

* Don't show suggestions without detail cards

* Don't add whitespace when pasting a token

* Don't update app's collection all the time

* Add data app related selectors to dashboard selectors

* Add redux state to keep title template changes

* Update page title template on page save

* Wire up `DataAppPageTitle` with dashboard code

* Fix unit tests

* Reset state on exiting editing mode

* Add back button to template data picker

* Add `PanePicker` UI component

* Add basic `RawDataPanePicker` component

* displays databases, schemas, tables
* allows picking a database and a schema (no tables yet)

* Allow picking tables

Supports single-selection and multi-selection modes

* Auto-select single database schema

* Use `.ts` instead of `.tsx` for hook file

* Style `PanePicker`

* Add `onTablesChange` callback prop to data picker

* Use new data picker in new app modal

* Use new data picker in add data modal

* Remove `DataAppDataPicker`

Co-authored-by: default avatarKyle Doherty <5248953+kdoh@users.noreply.github.com>

* Make `DataPicker` controlled (#26018)

* Add data type picker to scaffolding data picker (#26019)

* Move `useSelectedTables` one level up

* Extract `DataPickerSelectedItem` type

* Add `type` to `DataPickerValue` type

* Handle `type` in `useDataPickerValue` hook

* Add data type picker step

* Add back button to raw data picker

* Add `CardPicker`

* Tweak spacing

* Move `VirtualTable` to shared picker types

* Temporary disable saved questions data type

* Simplify schema loading for models and questions

* Remove redundant `useMemo`

* Rename `RawDataPanePicker` to `RawDataPicker` (#26020)

* Add safety checks to scaffolding data picker (#26023)

* Pass available data types as a prop to the picker

* Use nested queries settings for data types list

* Offer models only if instance has at least one

* Check data access

* Break `DataPicker` into container and view

* Make `DataPicker` cover the whole container

* Add items filters API to data app scaffolding data picker (#26024)

* Uncomment saved questions data type

* Add data picker filters API types

* Apply data type filters

* Auto pick a data type when only one is available

* Export data picker types

* Add `DataAppScaffoldingDataPicker` component

* Use `DataAppScaffoldingDataPicker`

* Improve collection selection for scaffolding data picker (#26033)

* Add collection ID to data picker value type

* Handle collection ID in `onChange`

* Handle collection ID in `CardPicker`

* Automatically open root collection

* Add basic search to scaffolding data picker (#26039)

* Add data picker context

* Add global search to data picker

* Add basic search UI to new app modal

* Don't use database ID in search

* Scope search depending on selected data type

* Expose more data picker types

* Style search input

* Keep filtered out data types in mind for search

* Move `MIN_SEARCH_LENGTH` to constants file

* Fix can't select nested collection in card picker (#26122)

* Fix conflicts

Co-authored-by: default avatarKyle Doherty <5248953+kdoh@users.noreply.github.com>
parent d39dce81
No related branches found
No related tags found
No related merge requests found
Showing
with 926 additions and 5 deletions
......@@ -24,6 +24,10 @@ export function getCollectionVirtualSchemaId(collection, { isDatasets } = {}) {
);
}
export function getRootCollectionVirtualSchemaId({ isModels }) {
return getCollectionVirtualSchemaId(null, { isDatasets: isModels });
}
export function getQuestionVirtualTableId(card) {
return `card__${card.id}`;
}
......
......@@ -6,7 +6,7 @@ import { useScrollOnMount } from "metabase/hooks/use-scroll-on-mount";
import { BaseItemRoot } from "./SelectListItem.styled";
const propTypes = {
id: PropTypes.string.isRequired,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
name: PropTypes.string.isRequired,
onSelect: PropTypes.func.isRequired,
isSelected: PropTypes.bool,
......
......@@ -73,4 +73,5 @@ function BaseTree({
export const Tree = Object.assign(BaseTree, {
Node: DefaultTreeNode,
NodeList: TreeNodeList,
});
import styled from "@emotion/styled";
export const ListRoot = styled.ul``;
import React from "react";
import { useScrollOnMount } from "metabase/hooks/use-scroll-on-mount";
import { ITreeNodeItem, TreeNodeComponent } from "./types";
import { ListRoot } from "./TreeNodeList.styled";
interface TreeNodeListProps {
items: ITreeNodeItem[];
......@@ -8,14 +11,16 @@ interface TreeNodeListProps {
selectedId?: ITreeNodeItem["id"];
depth: number;
role?: string;
className?: string;
onToggleExpand: (id: ITreeNodeItem["id"]) => void;
onSelect?: (item: ITreeNodeItem) => void;
TreeNode: TreeNodeComponent;
}
export function TreeNodeList({
function BaseTreeNodeList({
items,
role,
className,
expandedIds,
selectedId,
depth,
......@@ -26,7 +31,7 @@ export function TreeNodeList({
const selectedRef = useScrollOnMount();
return (
<ul role={role}>
<ListRoot className={className} role={role}>
{items.map(item => {
const isSelected = selectedId === item.id;
const hasChildren =
......@@ -49,7 +54,7 @@ export function TreeNodeList({
depth={depth}
/>
{isExpanded && (
<TreeNodeList
<BaseTreeNodeList
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
items={item.children!}
expandedIds={expandedIds}
......@@ -63,6 +68,10 @@ export function TreeNodeList({
</React.Fragment>
);
})}
</ul>
</ListRoot>
);
}
export const TreeNodeList = Object.assign(BaseTreeNodeList, {
Root: ListRoot,
});
import styled from "@emotion/styled";
import SelectList from "metabase/components/SelectList";
export const StyledSelectList = styled(SelectList)`
width: 100%;
padding: 0 1rem;
`;
import React, { useCallback, useMemo } from "react";
import _ from "underscore";
import { connect } from "react-redux";
import Collections from "metabase/entities/collections";
import Schemas from "metabase/entities/schemas";
import { getUser } from "metabase/selectors/user";
import type { Collection, User } from "metabase-types/api";
import type { State } from "metabase-types/store";
import type Table from "metabase-lib/metadata/Table";
import type Schema from "metabase-lib/metadata/Schema";
import { getCollectionVirtualSchemaId } from "metabase-lib/metadata/utils/saved-questions";
import type {
DataPickerProps,
DataPickerSelectedItem,
VirtualTable,
} from "../types";
import useSelectedTables from "../useSelectedTables";
import { buildCollectionTree } from "./utils";
import CardPickerView from "./CardPickerView";
interface CardPickerOwnProps extends DataPickerProps {
targetModel: "model" | "question";
onBack?: () => void;
}
interface CardPickerStateProps {
currentUser: User;
}
interface CollectionsLoaderProps {
collectionTree: Collection[];
collections: Collection[];
rootCollection: Collection;
}
interface SchemaLoaderProps {
schema?: Schema & { tables: VirtualTable[] };
}
type CardPickerProps = CardPickerOwnProps &
CardPickerStateProps &
CollectionsLoaderProps &
SchemaLoaderProps;
function mapStateToProps(state: State) {
return {
currentUser: getUser(state),
};
}
function CardPickerContainer({
value,
collections,
collectionTree,
rootCollection,
schema: selectedSchema,
currentUser,
targetModel,
onChange,
onBack,
}: CardPickerProps) {
const { collectionId } = value;
const { selectedTableIds, toggleTableIdSelection } = useSelectedTables({
initialValues: value.tableIds,
mode: "multiple",
});
const collectionsMap = useMemo(
() => _.indexBy(collections, "id"),
[collections],
);
const tree = useMemo(
() =>
buildCollectionTree({
collections: collectionTree,
rootCollection,
currentUser,
targetModel,
}),
[collectionTree, rootCollection, currentUser, targetModel],
);
const selectedItems = useMemo(() => {
const items: DataPickerSelectedItem[] = [];
if (collectionId) {
items.push({ type: "collection", id: collectionId });
}
const tables: DataPickerSelectedItem[] = selectedTableIds.map(id => ({
type: "table",
id,
}));
items.push(...tables);
return items;
}, [collectionId, selectedTableIds]);
const handleSelectedCollectionChange = useCallback(
(id: Collection["id"]) => {
const collection = id === "root" ? rootCollection : collectionsMap[id];
if (collection) {
const schemaId = getCollectionVirtualSchemaId(collection, {
isDatasets: targetModel === "model",
});
onChange({ ...value, schemaId, collectionId: id, tableIds: [] });
}
},
[value, collectionsMap, rootCollection, targetModel, onChange],
);
const handleSelectedTablesChange = useCallback(
(tableId: Table["id"]) => {
const tableIds = toggleTableIdSelection(tableId);
onChange({ ...value, tableIds });
},
[value, toggleTableIdSelection, onChange],
);
return (
<CardPickerView
collectionTree={tree}
virtualTables={selectedSchema?.tables}
selectedItems={selectedItems}
targetModel={targetModel}
onSelectCollection={handleSelectedCollectionChange}
onSelectedVirtualTable={handleSelectedTablesChange}
onBack={onBack}
/>
);
}
export default _.compose(
Collections.load({
id: "root",
entityAlias: "rootCollection",
loadingAndErrorWrapper: false,
}),
Collections.loadList({
query: () => ({ tree: true }),
listName: "collectionTree",
}),
Collections.loadList({
listName: "collections",
}),
Schemas.load({
id: (state: State, props: CardPickerOwnProps) => props.value.schemaId,
loadingAndErrorWrapper: false,
}),
connect(mapStateToProps),
)(CardPickerContainer);
import React, { useCallback, useMemo } from "react";
import _ from "underscore";
import SelectList from "metabase/components/SelectList";
import { canonicalCollectionId } from "metabase/collections/utils";
import type { ITreeNodeItem } from "metabase/components/tree/types";
import type { Collection } from "metabase-types/api";
import type Table from "metabase-lib/metadata/Table";
import type { DataPickerSelectedItem, VirtualTable } from "../types";
import PanePicker from "../PanePicker";
import { StyledSelectList } from "./CardPicker.styled";
type TargetModel = "model" | "question";
interface CardPickerViewProps {
collectionTree: ITreeNodeItem[];
virtualTables?: VirtualTable[];
selectedItems: DataPickerSelectedItem[];
targetModel: TargetModel;
onSelectCollection: (id: Collection["id"]) => void;
onSelectedVirtualTable: (id: Table["id"]) => void;
onBack?: () => void;
}
function getTableIcon({
isSelected,
targetModel,
}: {
isSelected: boolean;
targetModel: TargetModel;
}) {
if (isSelected) {
return "check";
}
return targetModel === "model" ? "model" : "table2";
}
function TableSelectListItem({
table,
targetModel,
isSelected,
onSelect,
}: {
table: VirtualTable;
targetModel: "model" | "question";
isSelected: boolean;
onSelect: (id: Table["id"]) => void;
}) {
return (
<SelectList.Item
id={table.id}
name={table.display_name}
isSelected={isSelected}
icon={getTableIcon({ isSelected, targetModel })}
onSelect={onSelect}
>
{table.display_name}
</SelectList.Item>
);
}
function formatCollectionId(id: string | number | null) {
const canonicalId = canonicalCollectionId(id);
return canonicalId === null ? "root" : canonicalId;
}
function CardPickerView({
collectionTree,
virtualTables,
selectedItems,
targetModel,
onSelectCollection,
onSelectedVirtualTable,
onBack,
}: CardPickerViewProps) {
const { selectedCollectionId, selectedVirtualTableIds } = useMemo(() => {
const { collection: collections = [], table: tables = [] } = _.groupBy(
selectedItems,
"type",
);
const [collection] = collections;
return {
selectedCollectionId: collection?.id,
selectedVirtualTableIds: tables.map(table => table.id),
};
}, [selectedItems]);
const handlePanePickerSelect = useCallback(
(item: ITreeNodeItem) => {
onSelectCollection(formatCollectionId(item.id));
},
[onSelectCollection],
);
const renderVirtualTable = useCallback(
(table: VirtualTable) => (
<TableSelectListItem
key={table.id}
table={table}
targetModel={targetModel}
isSelected={selectedVirtualTableIds.includes(table.id)}
onSelect={onSelectedVirtualTable}
/>
),
[selectedVirtualTableIds, targetModel, onSelectedVirtualTable],
);
return (
<PanePicker
data={collectionTree}
selectedId={selectedCollectionId}
onSelect={handlePanePickerSelect}
onBack={onBack}
>
<StyledSelectList>
{virtualTables?.map?.(renderVirtualTable)}
</StyledSelectList>
</PanePicker>
);
}
export default CardPickerView;
export { default } from "./CardPickerContainer";
import _ from "underscore";
import {
PERSONAL_COLLECTIONS,
buildCollectionTree as _buildCollectionTree,
} from "metabase/entities/collections";
import {
isPersonalCollection,
nonPersonalOrArchivedCollection,
currentUserPersonalCollections,
} from "metabase/collections/utils";
import type {
Collection,
CollectionContentModel,
User,
} from "metabase-types/api";
function getOurAnalyticsCollection(collectionEntity: any) {
return {
...collectionEntity,
schemaName: "Everything else",
icon: "folder",
};
}
const ALL_PERSONAL_COLLECTIONS_ROOT = {
...PERSONAL_COLLECTIONS,
};
export function buildCollectionTree({
collections,
rootCollection,
currentUser,
targetModel = "question",
}: {
collections: Collection[];
rootCollection: Collection;
currentUser: User;
targetModel?: "model" | "question";
}) {
const preparedCollections: Collection[] = [];
const userPersonalCollections = currentUserPersonalCollections(
collections,
currentUser.id,
);
const nonPersonalOrArchivedCollections = collections.filter(
nonPersonalOrArchivedCollection,
);
preparedCollections.push(...userPersonalCollections);
preparedCollections.push(...nonPersonalOrArchivedCollections);
if (currentUser.is_superuser) {
const otherPersonalCollections = collections.filter(
collection =>
isPersonalCollection(collection) &&
collection.personal_owner_id !== currentUser.id,
);
if (otherPersonalCollections.length > 0) {
preparedCollections.push({
...ALL_PERSONAL_COLLECTIONS_ROOT,
children: otherPersonalCollections,
} as Collection);
}
}
const targetModels: CollectionContentModel[] =
targetModel === "model" ? ["dataset"] : ["card"];
const tree = _buildCollectionTree(preparedCollections, { targetModels });
if (rootCollection) {
tree.unshift(getOurAnalyticsCollection(rootCollection));
}
return tree;
}
import React, { useCallback, useMemo } from "react";
import { connect } from "react-redux";
import _ from "underscore";
import { getHasDataAccess } from "metabase/selectors/data";
import { getSetting } from "metabase/selectors/settings";
import { useOnMount } from "metabase/hooks/use-on-mount";
import Databases from "metabase/entities/databases";
import Search from "metabase/entities/search";
import type { State } from "metabase-types/store";
import {
getRootCollectionVirtualSchemaId,
SAVED_QUESTIONS_VIRTUAL_DB_ID,
} from "metabase-lib/metadata/utils/saved-questions";
import type {
DataPickerProps as DataPickerOwnProps,
DataPickerDataType,
} from "./types";
import { DataPickerContextProvider, useDataPicker } from "./DataPickerContext";
import { getDataTypes, DEFAULT_DATA_PICKER_FILTERS } from "./utils";
import DataPickerView from "./DataPickerView";
interface DataPickerStateProps {
hasNestedQueriesEnabled: boolean;
hasDataAccess: boolean;
}
interface SearchListLoaderProps {
search: unknown[];
}
type DataPickerProps = DataPickerOwnProps &
DataPickerStateProps &
SearchListLoaderProps;
function mapStateToProps(state: State) {
return {
hasNestedQueriesEnabled: getSetting(state, "enable-nested-queries"),
hasDataAccess: getHasDataAccess(state),
};
}
function DataPicker({
search: modelLookupResult,
filters: customFilters = {},
hasNestedQueriesEnabled,
hasDataAccess,
...props
}: DataPickerProps) {
const { onChange } = props;
const { search } = useDataPicker();
const filters = useMemo(
() => ({
...DEFAULT_DATA_PICKER_FILTERS,
...customFilters,
}),
[customFilters],
);
const dataTypes = useMemo(
() =>
getDataTypes({
hasModels: modelLookupResult.length > 0,
hasNestedQueriesEnabled,
}).filter(type => filters.types(type.id)),
[filters, modelLookupResult, hasNestedQueriesEnabled],
);
const handleDataTypeChange = useCallback(
(type: DataPickerDataType) => {
const isModels = type === "models";
const isUsingVirtualTables = isModels || type === "questions";
// When switching to models or questions,
// we want to automatically open Our analytics collection
const databaseId = isUsingVirtualTables
? SAVED_QUESTIONS_VIRTUAL_DB_ID
: undefined;
const schemaId = isUsingVirtualTables
? getRootCollectionVirtualSchemaId({ isModels })
: undefined;
const collectionId = isUsingVirtualTables ? "root" : undefined;
onChange({
type,
databaseId,
schemaId,
collectionId,
tableIds: [],
});
},
[onChange],
);
useOnMount(() => {
if (dataTypes.length === 1) {
handleDataTypeChange(dataTypes[0].id);
}
});
const handleBack = useCallback(() => {
onChange({
type: undefined,
databaseId: undefined,
schemaId: undefined,
tableIds: [],
});
}, [onChange]);
return (
<DataPickerView
{...props}
dataTypes={dataTypes}
searchQuery={search.query}
hasDataAccess={hasDataAccess}
onDataTypeChange={handleDataTypeChange}
onBack={handleBack}
/>
);
}
const DataPickerContainer = _.compose(
// Required for `hasDataAccess` check
Databases.loadList(),
// Lets the picker check there is
// at least one model, to offer for selection
Search.loadList({
query: {
models: "dataset",
limit: 1,
},
}),
connect(mapStateToProps),
)(DataPicker);
export default Object.assign(DataPickerContainer, {
Provider: DataPickerContextProvider,
});
import { createContext, useContext } from "react";
import _ from "underscore";
export interface IDataPickerContext {
search: {
query: string;
setQuery: (query: string) => void;
};
}
export const DataPickerContext = createContext<IDataPickerContext>({
search: {
query: "",
setQuery: _.noop,
},
});
export function useDataPicker() {
return useContext(DataPickerContext);
}
import React, { useMemo, useState } from "react";
import { DataPickerContext, IDataPickerContext } from "./DataPickerContext";
function DataPickerContextProvider({
children,
}: {
children: React.ReactNode;
}) {
const [searchQuery, setSearchQuery] = useState("");
const value: IDataPickerContext = useMemo(
() => ({
search: {
query: searchQuery,
setQuery: setSearchQuery,
},
}),
[searchQuery],
);
return (
<DataPickerContext.Provider value={value}>
{children}
</DataPickerContext.Provider>
);
}
export default DataPickerContextProvider;
export { useDataPicker } from "./DataPickerContext";
export { default as DataPickerContextProvider } from "./DataPickerContextProvider";
import styled from "@emotion/styled";
export const Root = styled.div`
display: flex;
flex: 1;
`;
export const EmptyStateContainer = styled.div`
display: flex;
flex: 1;
align-items: center;
justify-content: center;
`;
import React, { useMemo } from "react";
import { t } from "ttag";
import EmptyState from "metabase/components/EmptyState";
import { MIN_SEARCH_LENGTH } from "./constants";
import type { DataPickerProps, DataPickerDataType } from "./types";
import type { DataTypeInfoItem } from "./utils";
import CardPicker from "./CardPicker";
import DataTypePicker from "./DataTypePicker";
import DataSearch from "./DataSearch";
import RawDataPicker from "./RawDataPicker";
import { Root, EmptyStateContainer } from "./DataPickerView.styled";
interface DataPickerViewProps extends DataPickerProps {
dataTypes: DataTypeInfoItem[];
searchQuery: string;
hasDataAccess: boolean;
onDataTypeChange: (type: DataPickerDataType) => void;
onBack?: () => void;
}
function DataPickerViewContent({
dataTypes,
searchQuery,
hasDataAccess,
onDataTypeChange,
...props
}: DataPickerViewProps) {
const { value, onChange } = props;
const availableDataTypes = useMemo(
() => dataTypes.map(type => type.id),
[dataTypes],
);
if (!hasDataAccess) {
return (
<EmptyStateContainer>
<EmptyState
message={t`To pick some data, you'll need to add some first`}
icon="database"
/>
</EmptyStateContainer>
);
}
if (searchQuery.trim().length > MIN_SEARCH_LENGTH) {
return (
<DataSearch
value={value}
searchQuery={searchQuery}
availableDataTypes={availableDataTypes}
onChange={onChange}
/>
);
}
if (!value.type) {
return <DataTypePicker types={dataTypes} onChange={onDataTypeChange} />;
}
if (value.type === "raw-data") {
return <RawDataPicker {...props} />;
}
if (value.type === "models") {
return <CardPicker {...props} targetModel="model" />;
}
if (value.type === "questions") {
return <CardPicker {...props} targetModel="question" />;
}
return null;
}
function DataPickerView(props: DataPickerViewProps) {
return (
<Root>
<DataPickerViewContent {...props} />
</Root>
);
}
export default DataPickerView;
import React, { useCallback, useMemo } from "react";
import { SearchResults } from "metabase/query_builder/components/DataSelector/data-search";
import type { Collection } from "metabase-types/api";
import {
getCollectionVirtualSchemaId,
getQuestionVirtualTableId,
SAVED_QUESTIONS_VIRTUAL_DB_ID,
} from "metabase-lib/metadata/utils/saved-questions";
import { generateSchemaId } from "metabase-lib/metadata/utils/schema";
import { useDataPicker } from "../DataPickerContext";
import type { DataPickerValue, DataPickerDataType } from "../types";
interface DataSearchProps {
value: DataPickerValue;
searchQuery: string;
availableDataTypes: DataPickerDataType[];
onChange: (value: DataPickerValue) => void;
}
type TableSearchResult = {
database_id: number;
table_schema: string;
table_id: number;
model: "table" | "dataset" | "card";
collection: Collection | null;
};
type SearchModel = "card" | "dataset" | "table";
const DATA_TYPE_SEARCH_MODEL_MAP: Record<DataPickerDataType, SearchModel> = {
"raw-data": "table",
models: "dataset",
questions: "card",
};
function getDataTypeForSearchResult(
table: TableSearchResult,
): DataPickerDataType {
switch (table.model) {
case "table":
return "raw-data";
case "card":
return "questions";
case "dataset":
return "models";
}
}
function getValueForRawTable(table: TableSearchResult): DataPickerValue {
return {
type: "raw-data",
databaseId: table.database_id,
schemaId: generateSchemaId(table.database_id, table.table_schema),
collectionId: undefined,
tableIds: [table.table_id],
};
}
function getValueForVirtualTable(table: TableSearchResult): DataPickerValue {
const type = getDataTypeForSearchResult(table);
const schemaId = getCollectionVirtualSchemaId(table.collection, {
isDatasets: type === "models",
});
return {
type: "models",
databaseId: SAVED_QUESTIONS_VIRTUAL_DB_ID,
schemaId,
collectionId: table.collection?.id || "root",
tableIds: [getQuestionVirtualTableId(table)],
};
}
function getNextValue(table: TableSearchResult): DataPickerValue {
const type = getDataTypeForSearchResult(table);
const isVirtualTable = type === "models" || type === "questions";
return isVirtualTable
? getValueForVirtualTable(table)
: getValueForRawTable(table);
}
function DataSearch({
value,
searchQuery,
availableDataTypes,
onChange,
}: DataSearchProps) {
const { search } = useDataPicker();
const { setQuery } = search;
const searchModels: SearchModel[] = useMemo(() => {
if (!value.type) {
return availableDataTypes.map(type => DATA_TYPE_SEARCH_MODEL_MAP[type]);
}
return [DATA_TYPE_SEARCH_MODEL_MAP[value.type]];
}, [value.type, availableDataTypes]);
const onSelect = useCallback(
(table: TableSearchResult) => {
const nextValue = getNextValue(table);
onChange(nextValue);
setQuery("");
},
[onChange, setQuery],
);
return (
<SearchResults
searchModels={searchModels}
searchQuery={searchQuery.trim()}
onSelect={onSelect}
/>
);
}
export default DataSearch;
export { default } from "./DataSearch";
import styled from "@emotion/styled";
import Icon from "metabase/components/Icon";
import SelectList from "metabase/components/SelectList";
import { color } from "metabase/lib/colors";
import { space } from "metabase/styled-components/theme";
export const List = styled(SelectList)`
padding: ${space(0)} ${space(1)} 12px ${space(1)};
`;
export const ItemIcon = styled(Icon)`
color: ${color("text-dark")};
`;
export const TitleContainer = styled.div`
display: flex;
align-items: center;
`;
export const ItemTitle = styled.span`
color: ${color("text-dark")};
font-weight: 700;
font-size: 14px;
margin-left: ${space(1)};
`;
export const ItemDescriptionContainer = styled.div`
margin-top: ${space(0)};
`;
export const ItemDescription = styled.span`
color: ${color("text-light")};
font-weight: 700;
font-size: 12px;
`;
export const ItemContainer = styled(SelectList.BaseItem as any)`
&:hover {
${ItemIcon},
${ItemTitle},
${ItemDescription} {
color: ${color("text-white")};
}
}
`;
import React from "react";
import type { DataTypeInfoItem } from "../utils";
import type { DataPickerDataType } from "../types";
import {
List,
ItemContainer,
TitleContainer,
ItemIcon,
ItemTitle,
ItemDescriptionContainer,
ItemDescription,
} from "./DataTypePicker.styled";
interface DataTypePickerProps {
types: DataTypeInfoItem[];
onChange: (value: DataPickerDataType) => void;
}
interface ListItemProps extends DataTypeInfoItem {
onSelect: () => void;
}
function DataTypePickerListItem({
id,
name,
icon,
description,
onSelect,
}: ListItemProps) {
return (
<ItemContainer id={id} name={name} onSelect={onSelect}>
<TitleContainer>
<ItemIcon name={icon} size={18} />
<ItemTitle>{name}</ItemTitle>
</TitleContainer>
<ItemDescriptionContainer>
<ItemDescription>{description}</ItemDescription>
</ItemDescriptionContainer>
</ItemContainer>
);
}
function DataTypePicker({ types, onChange }: DataTypePickerProps) {
return (
<List>
{types.map(dataType => (
<DataTypePickerListItem
{...dataType}
key={dataType.id}
onSelect={() => onChange(dataType.id)}
/>
))}
</List>
);
}
export default DataTypePicker;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment