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 {
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 }[] {
const pks = [];
this.fields.forEach((field, index) => {
......
......@@ -9,6 +9,7 @@ import * as Urls from "metabase/lib/urls";
import type { Card } from "metabase-types/api";
import type Question from "metabase-lib/lib/Question";
import type Table from "metabase-lib/lib/metadata/Table";
import ModelActionDetails from "./ModelActionDetails";
import ModelInfoSidePanel from "./ModelInfoSidePanel";
......@@ -26,12 +27,13 @@ import {
interface Props {
model: Question;
mainTable?: Table | null;
onChangeModel: (model: Card) => void;
}
type ModelTab = "schema" | "actions" | "usage";
function ModelDetailPage({ model, onChangeModel }: Props) {
function ModelDetailPage({ model, mainTable, onChangeModel }: Props) {
const [tab, setTab] = useState<ModelTab>("usage");
const modelCard = model.card();
......@@ -93,6 +95,7 @@ function ModelDetailPage({ model, onChangeModel }: Props) {
</ModelMain>
<ModelInfoSidePanel
model={model}
mainTable={mainTable}
onChangeDescription={handleChangeDescription}
/>
</RootLayout>
......
......@@ -26,13 +26,17 @@ export const ModelInfoTitle = styled.span`
padding-left: 4px;
`;
const commonInfoTextStyle = css`
export const valueBlockStyle = css`
display: block;
margin-top: 0.5rem;
color: ${color("text-medium")};
padding-left: 4px;
`;
const commonInfoTextStyle = css`
${valueBlockStyle}
color: ${color("text-medium")};
`;
export const ModelInfoText = styled.span`
${commonInfoTextStyle}
`;
......
import React, { useMemo } from "react";
import React from "react";
import { t } from "ttag";
import * as Urls from "metabase/lib/urls";
import type { Card, Table } from "metabase-types/api";
import type { Card } from "metabase-types/api";
import type Question from "metabase-lib/lib/Question";
import type Table from "metabase-lib/lib/metadata/Table";
import ModelRelationships from "./ModelRelationships";
import {
ModelInfoPanel,
ModelInfoTitle,
......@@ -17,17 +17,16 @@ import {
interface Props {
model: Question;
mainTable?: Table | null;
onChangeDescription: (description: string | null) => void;
}
function ModelInfoSidePanel({ model, onChangeDescription }: Props) {
function ModelInfoSidePanel({ model, mainTable, onChangeDescription }: Props) {
const modelCard = model.card() as Card;
const canWrite = model.canWrite();
const description = model.description();
const backingTable = useMemo(() => model.query().sourceTable(), [model]);
return (
<ModelInfoPanel>
<ModelInfoSection>
......@@ -43,19 +42,18 @@ function ModelInfoSidePanel({ model, onChangeDescription }: Props) {
onChange={onChangeDescription}
/>
</ModelInfoSection>
<ModelRelationships model={model} mainTable={mainTable} />
{modelCard.creator && (
<ModelInfoSection>
<ModelInfoTitle>{t`Contact`}</ModelInfoTitle>
<ModelInfoText>{modelCard.creator.common_name}</ModelInfoText>
</ModelInfoSection>
)}
{backingTable && (
{mainTable && (
<ModelInfoSection>
<ModelInfoTitle>{t`Backing table`}</ModelInfoTitle>
<ModelInfoLink
to={backingTable.newQuestion().getUrl({ clean: false })}
>
{backingTable.displayName()}
<ModelInfoLink to={mainTable.newQuestion().getUrl({ clean: false })}>
{mainTable.displayName()}
</ModelInfoLink>
</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 { connect } from "react-redux";
......@@ -7,6 +7,7 @@ import { useOnMount } from "metabase/hooks/use-on-mount";
import { getMetadata } from "metabase/selectors/metadata";
import Questions from "metabase/entities/questions";
import Tables from "metabase/entities/tables";
import { loadMetadataForCard } from "metabase/query_builder/actions";
......@@ -17,6 +18,7 @@ import type { Card as LegacyCardType } from "metabase-types/types/Card";
import type { State } from "metabase-types/store";
import Question from "metabase-lib/lib/Question";
import Table from "metabase-lib/lib/metadata/Table";
type OwnProps = {
params: {
......@@ -34,6 +36,7 @@ type StateProps = {
type DispatchProps = {
loadMetadataForCard: (card: LegacyCardType) => void;
fetchTableForeignKeys: (params: { id: Table["id"] }) => void;
onChangeModel: (card: Card) => void;
};
......@@ -47,15 +50,41 @@ function mapStateToProps(state: State, props: OwnProps & EntityLoaderProps) {
const mapDispatchToProps = {
loadMetadataForCard,
fetchTableForeignKeys: Tables.actions.fetchForeignKeys,
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(() => {
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(
......
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