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

Prototype model relationships section for the detail page (#25524)

* Fetch foreign keys for main model table

* Add `foreignTables` method to `Table`

* Show related tables in the info panel

* Fix duplicates
parent 1ba5fe34
No related branches found
No related tags found
No related merge requests found
...@@ -139,6 +139,16 @@ class TableInner extends Base { ...@@ -139,6 +139,16 @@ class TableInner extends Base {
return fks.map(fk => new Table(fk.origin.table)); return fks.map(fk => new Table(fk.origin.table));
} }
foreignTables(): Table[] {
if (!Array.isArray(this.fields)) {
return [];
}
return this.fields
.filter(field => field.isFK() && field.fk_target_field_id)
.map(field => this.metadata.field(field.fk_target_field_id)?.table)
.filter(Boolean);
}
primaryKeys(): { field: Field; index: number }[] { primaryKeys(): { field: Field; index: number }[] {
const pks = []; const pks = [];
this.fields.forEach((field, index) => { this.fields.forEach((field, index) => {
......
...@@ -9,6 +9,7 @@ import * as Urls from "metabase/lib/urls"; ...@@ -9,6 +9,7 @@ import * as Urls from "metabase/lib/urls";
import type { Card } from "metabase-types/api"; import type { Card } from "metabase-types/api";
import type Question from "metabase-lib/lib/Question"; import type Question from "metabase-lib/lib/Question";
import type Table from "metabase-lib/lib/metadata/Table";
import ModelActionDetails from "./ModelActionDetails"; import ModelActionDetails from "./ModelActionDetails";
import ModelInfoSidePanel from "./ModelInfoSidePanel"; import ModelInfoSidePanel from "./ModelInfoSidePanel";
...@@ -26,12 +27,13 @@ import { ...@@ -26,12 +27,13 @@ import {
interface Props { interface Props {
model: Question; model: Question;
mainTable?: Table | null;
onChangeModel: (model: Card) => void; onChangeModel: (model: Card) => void;
} }
type ModelTab = "schema" | "actions" | "usage"; type ModelTab = "schema" | "actions" | "usage";
function ModelDetailPage({ model, onChangeModel }: Props) { function ModelDetailPage({ model, mainTable, onChangeModel }: Props) {
const [tab, setTab] = useState<ModelTab>("usage"); const [tab, setTab] = useState<ModelTab>("usage");
const modelCard = model.card(); const modelCard = model.card();
...@@ -93,6 +95,7 @@ function ModelDetailPage({ model, onChangeModel }: Props) { ...@@ -93,6 +95,7 @@ function ModelDetailPage({ model, onChangeModel }: Props) {
</ModelMain> </ModelMain>
<ModelInfoSidePanel <ModelInfoSidePanel
model={model} model={model}
mainTable={mainTable}
onChangeDescription={handleChangeDescription} onChangeDescription={handleChangeDescription}
/> />
</RootLayout> </RootLayout>
......
...@@ -26,13 +26,17 @@ export const ModelInfoTitle = styled.span` ...@@ -26,13 +26,17 @@ export const ModelInfoTitle = styled.span`
padding-left: 4px; padding-left: 4px;
`; `;
const commonInfoTextStyle = css` export const valueBlockStyle = css`
display: block; display: block;
margin-top: 0.5rem; margin-top: 0.5rem;
color: ${color("text-medium")};
padding-left: 4px; padding-left: 4px;
`; `;
const commonInfoTextStyle = css`
${valueBlockStyle}
color: ${color("text-medium")};
`;
export const ModelInfoText = styled.span` export const ModelInfoText = styled.span`
${commonInfoTextStyle} ${commonInfoTextStyle}
`; `;
......
import React, { useMemo } from "react"; import React from "react";
import { t } from "ttag"; import { t } from "ttag";
import * as Urls from "metabase/lib/urls"; import type { Card } from "metabase-types/api";
import type { Card, Table } from "metabase-types/api";
import type Question from "metabase-lib/lib/Question"; import type Question from "metabase-lib/lib/Question";
import type Table from "metabase-lib/lib/metadata/Table";
import ModelRelationships from "./ModelRelationships";
import { import {
ModelInfoPanel, ModelInfoPanel,
ModelInfoTitle, ModelInfoTitle,
...@@ -17,17 +17,16 @@ import { ...@@ -17,17 +17,16 @@ import {
interface Props { interface Props {
model: Question; model: Question;
mainTable?: Table | null;
onChangeDescription: (description: string | null) => void; onChangeDescription: (description: string | null) => void;
} }
function ModelInfoSidePanel({ model, onChangeDescription }: Props) { function ModelInfoSidePanel({ model, mainTable, onChangeDescription }: Props) {
const modelCard = model.card() as Card; const modelCard = model.card() as Card;
const canWrite = model.canWrite(); const canWrite = model.canWrite();
const description = model.description(); const description = model.description();
const backingTable = useMemo(() => model.query().sourceTable(), [model]);
return ( return (
<ModelInfoPanel> <ModelInfoPanel>
<ModelInfoSection> <ModelInfoSection>
...@@ -43,19 +42,18 @@ function ModelInfoSidePanel({ model, onChangeDescription }: Props) { ...@@ -43,19 +42,18 @@ function ModelInfoSidePanel({ model, onChangeDescription }: Props) {
onChange={onChangeDescription} onChange={onChangeDescription}
/> />
</ModelInfoSection> </ModelInfoSection>
<ModelRelationships model={model} mainTable={mainTable} />
{modelCard.creator && ( {modelCard.creator && (
<ModelInfoSection> <ModelInfoSection>
<ModelInfoTitle>{t`Contact`}</ModelInfoTitle> <ModelInfoTitle>{t`Contact`}</ModelInfoTitle>
<ModelInfoText>{modelCard.creator.common_name}</ModelInfoText> <ModelInfoText>{modelCard.creator.common_name}</ModelInfoText>
</ModelInfoSection> </ModelInfoSection>
)} )}
{backingTable && ( {mainTable && (
<ModelInfoSection> <ModelInfoSection>
<ModelInfoTitle>{t`Backing table`}</ModelInfoTitle> <ModelInfoTitle>{t`Backing table`}</ModelInfoTitle>
<ModelInfoLink <ModelInfoLink to={mainTable.newQuestion().getUrl({ clean: false })}>
to={backingTable.newQuestion().getUrl({ clean: false })} {mainTable.displayName()}
>
{backingTable.displayName()}
</ModelInfoLink> </ModelInfoLink>
</ModelInfoSection> </ModelInfoSection>
)} )}
......
import styled from "@emotion/styled";
import Link from "metabase/core/components/Link";
import Icon from "metabase/components/Icon";
import { color, darken } from "metabase/lib/colors";
import { valueBlockStyle } from "./ModelInfoSidePanel.styled";
export const List = styled.ul`
${valueBlockStyle}
li:not(:first-of-type) {
margin-top: 6px;
}
`;
export const ListItemName = styled.span`
display: block;
`;
export const ListItemLink = styled(Link)`
display: flex;
align-items: center;
color: ${color("brand")};
${ListItemName} {
margin-left: 4px;
}
&:hover {
color: ${darken("brand")};
}
`;
import React, { useMemo } from "react";
import { t } from "ttag";
import _ from "underscore";
import Icon from "metabase/components/Icon";
import type Question from "metabase-lib/lib/Question";
import type Table from "metabase-lib/lib/metadata/Table";
import { ModelInfoTitle, ModelInfoSection } from "./ModelInfoSidePanel.styled";
import { List, ListItemLink, ListItemName } from "./ModelRelationships.styled";
interface Props {
model: Question;
mainTable?: Table | null;
}
function ModelRelationships({ model, mainTable }: Props) {
const relatedTables = useMemo(() => {
const tablesMainTablePointsTo = model.table()?.foreignTables() || [];
const tablesPointingToMainTable = mainTable?.connectedTables() || [];
return _.uniq(
[...tablesMainTablePointsTo, ...tablesPointingToMainTable],
table => table.id,
);
}, [model, mainTable]);
if (relatedTables.length <= 0) {
return null;
}
return (
<ModelInfoSection>
<ModelInfoTitle>{t`Relationships`}</ModelInfoTitle>
<List>
{relatedTables.map(table => (
<li key={table.id}>
<ListItemLink to={table.newQuestion().getUrl()}>
<Icon name="table" />
<ListItemName>{table.displayName()}</ListItemName>
</ListItemLink>
</li>
))}
</List>
</ModelInfoSection>
);
}
export default ModelRelationships;
import React from "react"; import React, { useEffect, useMemo, useState } from "react";
import _ from "underscore"; import _ from "underscore";
import { connect } from "react-redux"; import { connect } from "react-redux";
...@@ -7,6 +7,7 @@ import { useOnMount } from "metabase/hooks/use-on-mount"; ...@@ -7,6 +7,7 @@ import { useOnMount } from "metabase/hooks/use-on-mount";
import { getMetadata } from "metabase/selectors/metadata"; import { getMetadata } from "metabase/selectors/metadata";
import Questions from "metabase/entities/questions"; import Questions from "metabase/entities/questions";
import Tables from "metabase/entities/tables";
import { loadMetadataForCard } from "metabase/query_builder/actions"; import { loadMetadataForCard } from "metabase/query_builder/actions";
...@@ -17,6 +18,7 @@ import type { Card as LegacyCardType } from "metabase-types/types/Card"; ...@@ -17,6 +18,7 @@ import type { Card as LegacyCardType } from "metabase-types/types/Card";
import type { State } from "metabase-types/store"; import type { State } from "metabase-types/store";
import Question from "metabase-lib/lib/Question"; import Question from "metabase-lib/lib/Question";
import Table from "metabase-lib/lib/metadata/Table";
type OwnProps = { type OwnProps = {
params: { params: {
...@@ -34,6 +36,7 @@ type StateProps = { ...@@ -34,6 +36,7 @@ type StateProps = {
type DispatchProps = { type DispatchProps = {
loadMetadataForCard: (card: LegacyCardType) => void; loadMetadataForCard: (card: LegacyCardType) => void;
fetchTableForeignKeys: (params: { id: Table["id"] }) => void;
onChangeModel: (card: Card) => void; onChangeModel: (card: Card) => void;
}; };
...@@ -47,15 +50,41 @@ function mapStateToProps(state: State, props: OwnProps & EntityLoaderProps) { ...@@ -47,15 +50,41 @@ function mapStateToProps(state: State, props: OwnProps & EntityLoaderProps) {
const mapDispatchToProps = { const mapDispatchToProps = {
loadMetadataForCard, loadMetadataForCard,
fetchTableForeignKeys: Tables.actions.fetchForeignKeys,
onChangeModel: Questions.actions.update, onChangeModel: Questions.actions.update,
}; };
function ModelDetailPage({ model, loadMetadataForCard, onChangeModel }: Props) { function ModelDetailPage({
model,
loadMetadataForCard,
fetchTableForeignKeys,
onChangeModel,
}: Props) {
const [hasFetchedTableMetadata, setHasFetchedTableMetadata] = useState(false);
const mainTable = useMemo(
() => (model.isStructured() ? model.query().sourceTable() : null),
[model],
);
useOnMount(() => { useOnMount(() => {
loadMetadataForCard(model.card()); loadMetadataForCard(model.card());
}); });
return <ModelDetailPageView model={model} onChangeModel={onChangeModel} />; useEffect(() => {
if (mainTable && !hasFetchedTableMetadata) {
setHasFetchedTableMetadata(true);
fetchTableForeignKeys({ id: mainTable.id });
}
}, [mainTable, hasFetchedTableMetadata, fetchTableForeignKeys]);
return (
<ModelDetailPageView
model={model}
mainTable={mainTable}
onChangeModel={onChangeModel}
/>
);
} }
export default _.compose( export default _.compose(
......
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