Skip to content

Commit

Permalink
Table Picker (#40509)
Browse files Browse the repository at this point in the history
* Generic types

* Generic types

* Generic types + make generateKey a prop

* Generic types

* Extract AutoScrollBox.styled.tsx

* Move AutoScrollBox to a separate directory

* Generic types

* Generic types

* Generic types

* Unhookify searchFilter

* Generic types

* Generic types

* Generic types

* Generic types

* Generic types

* Generic types

* Generic types

* Generic types

* Finish dealing with a cast

* Destructure import

* Inline type

* Generic types

* Remove cast

* Remove commented code

* Remove redundant fallback

* Move CollectionPicker out of EntityPicker

* Avoid as unknown

* Remove TODOs

* Rename TisFolder to IsFolder

* Fix any

* Fix anys

* Remove NestedItemPicker's storybook

* Remove a cast

* Remove a cast

* Remove a cast

* Revert "Remove a cast"

This reverts commit b762d0f.

* Use extends SearchModelType instead of extends string everywhere

* Revert "Use extends SearchModelType instead of extends string everywhere"

This reverts commit 708190d.

* Fix SearchResult["available_models"] type

* Handle options.allowCreateNew

* Add missing description attribute to Database type

* Move allowCreateNew to CollectionPickerOptions

* Add missing description attribute in Database mock

* Add types

* Add TableList component

* Add SchemaList component

* Fix naming

* Add DatabaseList component

* Add NotebookDataItemPickerResolver

* Fix typing

* Add TablePicker and NotebookDataPickerModal

* Update types

* Fix types

* Add folder type

* Fix initial state

* Update title

* Make query model-dependent

* Render tables

* Fix schema icon

* Adjust key generation

* Rename utilts to utils

* Use ItemList error prop

* Use value prop

* Rename NotebookDataPickerModal to DataPickerModal

* Automatically open datapicker if there is no value

* Highlight current item

* Rename value to initial value

* Fix item highlighting

* Rework TablePicker state

* Leave TODOs for names

* Fix selecting items

* Fix highlighting selected item

* Hide confirmation button

* Add fetchMetadata to tableApi

* Fix Table['schema'] type

* Fetch table metadata upon selection

* Remove options from DataPickerModal

* Fix collectionId typing in Question

* Add collectionId prop

* Remove title prop

* Add todo

* Fix confirmation button

* Remove old DataSourceSelector

* Bring back useSchemaListQuery

* Fix crash

* Fix picker not opening

* Hide dbs list and schemas list if there's only 1

* Remove unused ref

* Remove unused DataPickerListResolver

* Remove unused types

* Change conditional rendering to allow for loading state

* Revert collection-related changes

* Use RTK

* Avoid using stale data

* Fix highlighted db

* Remove todo

* Remove unused type

* Remove useSchemaListQuery

* Rename Value to TablePickerValue

* Move isValueEqual to utils and rename it to isTablePickerValueEqual

* Improve TODO

* Remove unused options prop

* Introduce tablePickerValueFromTable

* Avoid having 2 table metadata requests

* Update comment

* Update comment

* Remove unused function

* Allow null in tablePickerValueFromTable

* Extract helpers

* Rename schemaId to schemaName

* Remove description

* Improve naming

* Simplify types

* Add explanatory comments

* Fall back to empty state when there are no items

* Account for null schemas

* Account for null schema in tests

* Revert "Account for null schema in tests"

This reverts commit 0c96d1c.

* Revert "Account for null schemas"

This reverts commit 4f5c4e9.

* Improve typing around schema name

* Refactor auto-select logic

* Refactor

* Avoid comment

* Make useAutoSelectOnlyItem generic

* Do not enforce presence of at least 1 tab with TypeScript

* Fix types

* Remove obsolete comment
  • Loading branch information
kamilmielnik committed Apr 9, 2024
1 parent 108c51d commit 5f7f09c
Show file tree
Hide file tree
Showing 27 changed files with 659 additions and 62 deletions.
4 changes: 2 additions & 2 deletions frontend/src/metabase-lib/v1/Question.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,11 +492,11 @@ class Question {
return this.setCard(assoc(this.card(), "name", name));
}

collectionId(): number | null | undefined {
collectionId(): CollectionId | null | undefined {
return this._card && this._card.collection_id;
}

setCollectionId(collectionId: number | null | undefined) {
setCollectionId(collectionId: CollectionId | null | undefined) {
return this.setCard(assoc(this.card(), "collection_id", collectionId));
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/metabase-lib/v1/metadata/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class Schema {
}

displayName() {
return this.name ? titleize(humanize(this.name)) : null;
return titleize(humanize(this.name));
}

getTables() {
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/metabase-types/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ import type { Field, FieldDimension, FieldId } from "./field";
import type { Metric, MetricId } from "./metric";
import type { Segment, SegmentId } from "./segment";
import type { NativeQuerySnippet } from "./snippets";
import type { ForeignKey, Schema, SchemaId, Table, TableId } from "./table";
import type {
ForeignKey,
Schema,
SchemaId,
SchemaName,
Table,
TableId,
} from "./table";
import type { Timeline, TimelineEventId } from "./timeline";
import type { User } from "./user";

Expand Down Expand Up @@ -41,7 +48,7 @@ export interface NormalizedTable
segments?: SegmentId[];
metrics?: MetricId[];
schema?: SchemaId;
schema_name?: string;
schema_name?: SchemaName;
}

export interface NormalizedForeignKey
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/metabase-types/api/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface Table {
db_id: DatabaseId;
db?: Database;

schema: string;
schema: SchemaName;

fks?: ForeignKey[];
fields?: Field[];
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/metabase/api/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
ListDatabaseSchemaTablesRequest,
ListDatabaseSchemasRequest,
ListVirtualDatabaseTablesRequest,
SchemaName,
} from "metabase-types/api";

import { Api } from "./api";
Expand Down Expand Up @@ -54,7 +55,10 @@ export const databaseApi = Api.injectEndpoints({
listTag("field"),
],
}),
listDatabaseSchemas: builder.query<string[], ListDatabaseSchemasRequest>({
listDatabaseSchemas: builder.query<
SchemaName[],
ListDatabaseSchemasRequest
>({
query: ({ id, ...body }) => ({
method: "GET",
url: `/api/database/${id}/schemas`,
Expand All @@ -65,7 +69,7 @@ export const databaseApi = Api.injectEndpoints({
...schemas.map(schema => idTag("schema", schema)),
],
}),
listSyncableDatabaseSchemas: builder.query<string[], DatabaseId>({
listSyncableDatabaseSchemas: builder.query<SchemaName[], DatabaseId>({
query: id => ({
method: "GET",
url: `/api/database/${id}/syncable_schemas`,
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/metabase/api/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
GetTableRequest,
Table,
TableId,
TableListQuery,
UpdateTableFieldsOrderRequest,
UpdateTableListRequest,
UpdateTableRequest,
Expand All @@ -14,10 +15,11 @@ import { idTag, invalidateTags, listTag, tag } from "./tags";

export const tableApi = Api.injectEndpoints({
endpoints: builder => ({
listTables: builder.query<Table[], void>({
query: () => ({
listTables: builder.query<Table[], TableListQuery | void>({
query: body => ({
method: "GET",
url: "/api/table",
body,
}),
providesTags: (tables = []) => [
listTag("table"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,19 @@ export const CollectionPickerModal = ({
}
};

const modalActions = [
<Button
key="collection-on-the-go"
miw="21rem"
onClick={openCreateDialog}
leftIcon={<Icon name="add" />}
disabled={selectedItem?.can_write === false}
>
{t`Create a new collection`}
</Button>,
];
const modalActions = options.allowCreateNew
? [
<Button
key="collection-on-the-go"
miw="21rem"
onClick={openCreateDialog}
leftIcon={<Icon name="add" />}
disabled={selectedItem?.can_write === false}
>
{t`Create a new collection`}
</Button>,
]
: [];

const tabs: [EntityTab<SearchModelType>] = [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type CollectionPickerItem = TypeWithModel<
};

export type CollectionPickerOptions = EntityPickerModalOptions & {
allowCreateNew?: boolean;
showPersonalCollections?: boolean;
showRootCollection?: boolean;
namespace?: "snippets";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useCallback } from "react";
import { t } from "ttag";
import _ from "underscore";

import type { CollectionId, TableId } from "metabase-types/api";

import type { EntityPickerModalOptions, EntityTab } from "../../EntityPicker";
import { EntityPickerModal, defaultOptions } from "../../EntityPicker";
import type { NotebookDataPickerValueItem, TablePickerValue } from "../types";

import { TablePicker } from "./TablePicker";

interface Props {
/**
* TODO: use this prop in https://github.com/metabase/metabase/issues/40719
*/
collectionId: CollectionId | null | undefined;
value: TablePickerValue | null;
onChange: (value: TableId) => void;
onClose: () => void;
}

const options: EntityPickerModalOptions = {
...defaultOptions,
hasConfirmButtons: false,
};

export const DataPickerModal = ({ value, onChange, onClose }: Props) => {
const handleItemChange = useCallback(
(item: NotebookDataPickerValueItem) => {
onChange(item.id);
onClose();
},
[onChange, onClose],
);

const tabs: EntityTab<NotebookDataPickerValueItem["model"]>[] = [
{
displayName: t`Tables`,
model: "table",
icon: "table",
element: <TablePicker value={value} onChange={handleItemChange} />,
},
];

return (
<EntityPickerModal
canSelectItem
options={options}
selectedItem={null}
tabs={tabs}
title={t`Pick your starting data`}
onClose={onClose}
onConfirm={_.noop} // onConfirm is unused when options.hasConfirmButtons is falsy
onItemSelect={handleItemChange}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useMemo } from "react";

import type { Database } from "metabase-types/api";

import { ItemList, ListBox } from "../../EntityPicker";
import { useAutoSelectOnlyItem } from "../hooks";
import type { NotebookDataPickerFolderItem } from "../types";

interface Props {
databases: Database[] | undefined;
error: unknown;
isCurrentLevel: boolean;
isLoading: boolean;
selectedItem: NotebookDataPickerFolderItem | null;
onClick: (item: NotebookDataPickerFolderItem) => void;
}

const isFolder = () => true;

export const DatabaseList = ({
databases,
error,
isCurrentLevel,
isLoading,
selectedItem,
onClick,
}: Props) => {
const items: NotebookDataPickerFolderItem[] | undefined = useMemo(() => {
return databases?.map(database => ({
id: database.id,
model: "database",
name: database.name,
}));
}, [databases]);

const hasOnly1Item = useAutoSelectOnlyItem(items, onClick);

if (!isLoading && !error && hasOnly1Item) {
return null;
}

return (
<ListBox data-testid="item-picker-level-0">
<ItemList
error={error}
isCurrentLevel={isCurrentLevel}
isFolder={isFolder}
isLoading={isLoading}
items={items}
selectedItem={selectedItem}
onClick={onClick}
/>
</ListBox>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useMemo } from "react";

import type { SchemaName } from "metabase-types/api";

import { ItemList, ListBox } from "../../EntityPicker";
import { useAutoSelectOnlyItem } from "../hooks";
import type { NotebookDataPickerFolderItem } from "../types";
import { getSchemaDisplayName } from "../utils";

interface Props {
error: unknown;
isCurrentLevel: boolean;
isLoading: boolean;
schemas: SchemaName[] | undefined;
selectedItem: NotebookDataPickerFolderItem | null;
onClick: (item: NotebookDataPickerFolderItem) => void;
}

const isFolder = () => true;

export const SchemaList = ({
error,
isCurrentLevel,
isLoading,
schemas,
selectedItem,
onClick,
}: Props) => {
const items: NotebookDataPickerFolderItem[] | undefined = useMemo(() => {
return schemas?.map(schema => ({
id: schema,
model: "schema",
name: getSchemaDisplayName(schema),
}));
}, [schemas]);

const hasOnly1Item = useAutoSelectOnlyItem(items, onClick);

if (!isLoading && !error && hasOnly1Item) {
return null;
}

return (
<ListBox data-testid="item-picker-level-1">
<ItemList
error={error}
isCurrentLevel={isCurrentLevel}
isFolder={isFolder}
isLoading={isLoading}
items={items}
selectedItem={selectedItem}
onClick={onClick}
/>
</ListBox>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useMemo } from "react";

import type { Table } from "metabase-types/api";

import { ItemList, ListBox } from "../../EntityPicker";
import type { NotebookDataPickerValueItem } from "../types";

interface Props {
error: unknown;
isLoading: boolean;
isCurrentLevel: boolean;
selectedItem: NotebookDataPickerValueItem | null;
tables: Table[] | undefined;
onClick: (item: NotebookDataPickerValueItem) => void;
}

const isFolder = () => false;

export const TableList = ({
error,
isLoading,
isCurrentLevel,
selectedItem,
tables,
onClick,
}: Props) => {
const items: NotebookDataPickerValueItem[] | undefined = useMemo(() => {
return tables?.map(table => ({
id: table.id,
model: "table",
name: table.display_name,
}));
}, [tables]);

return (
<ListBox data-testid="item-picker-level-2">
<ItemList
error={error}
isCurrentLevel={isCurrentLevel}
isFolder={isFolder}
isLoading={isLoading}
items={items}
selectedItem={selectedItem}
onClick={onClick}
/>
</ListBox>
);
};
Loading

0 comments on commit 5f7f09c

Please sign in to comment.