Unverified Commit 65defde5 authored by Marcos Iglesias's avatar Marcos Iglesias Committed by GitHub

feat: Adds related dashboards tab to the Table detail page (#470)

* Renders empty dashboards tab and adds some basic tests

* Adds new object to the type, testing request that doesn't work

* Merges dashboard data into TableMetadata object

* Reusing the DashboardResource type instead of creating a new one

* Removing msg and status_code attributes from the merged response as per Tamika's comment

* Updates first of list top border
parent bb1966af
...@@ -26,10 +26,4 @@ ...@@ -26,10 +26,4 @@
margin-bottom: $spacer-4; margin-bottom: $spacer-4;
} }
} }
.tabs-component {
.tab-content .list-group-item:first-child {
border-top: none;
}
}
} }
...@@ -25,6 +25,7 @@ describe('ExploreButton', () => { ...@@ -25,6 +25,7 @@ describe('ExploreButton', () => {
badges: [], badges: [],
cluster: 'cluster', cluster: 'cluster',
columns: [], columns: [],
dashboards: [],
database: 'database', database: 'database',
is_editable: false, is_editable: false,
is_view: false, is_view: false,
......
export const PROGRMMATIC_DESC_HEADER = 'Read-only information, auto-generated' export const PROGRMMATIC_DESC_HEADER = 'Read-only information, auto-generated';
export const ERROR_MESSAGE = 'Something went wrong...';
import * as React from 'react';
import * as History from 'history';
import { mount } from 'enzyme';
import { getMockRouterProps } from '../../fixtures/mockRouter';
import { tableMetadata } from '../../fixtures/metadata/table';
import LoadingSpinner from '../common/LoadingSpinner';
import { TableDetail, TableDetailProps, MatchProps } from './';
describe('TableDetail', () => {
const setup = (propOverrides?: Partial<TableDetailProps>, location?: Partial<History.Location>) => {
const routerProps = getMockRouterProps<MatchProps>({
"cluster":"gold",
"database":"hive",
"schema":"base",
"table":"rides"
}, location);
const props = {
isLoading: false,
statusCode: 200,
tableData: tableMetadata,
getTableData: jest.fn(),
...routerProps,
...propOverrides,
};
const wrapper = mount<TableDetail>(<TableDetail {...props} />);
return { props, wrapper };
};
describe('render', () => {
it('should render without problems', () => {
expect(() => {setup();}).not.toThrow();
});
it('should render the Loading Spinner', () => {
const {wrapper} = setup();
const expected = 1;
const actual = wrapper.find(LoadingSpinner).length;
expect(actual).toEqual(expected);
});
});
describe('lifecycle', () => {
describe('when mounted', () => {
it('calls loadDashboard with uri from state', () => {
const { props } = setup();
const expected = 1;
const actual = (props.getTableData as jest.Mock).mock.calls.length;
expect(actual).toEqual(expected);
});
});
});
});
...@@ -12,13 +12,16 @@ import AppConfig from 'config/config'; ...@@ -12,13 +12,16 @@ import AppConfig from 'config/config';
import BadgeList from 'components/common/BadgeList'; import BadgeList from 'components/common/BadgeList';
import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon'; import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon';
import Breadcrumb from 'components/common/Breadcrumb'; import Breadcrumb from 'components/common/Breadcrumb';
import TabsComponent from 'components/common/TabsComponent';
import EditableText from 'components/common/EditableText';
import LoadingSpinner from 'components/common/LoadingSpinner';
import Flag from 'components/common/Flag';
import ResourceList from 'components/common/ResourceList';
import DataPreviewButton from 'components/TableDetail/DataPreviewButton'; import DataPreviewButton from 'components/TableDetail/DataPreviewButton';
import ColumnList from 'components/TableDetail/ColumnList'; import ColumnList from 'components/TableDetail/ColumnList';
import EditableText from 'components/common/EditableText';
import ExploreButton from 'components/TableDetail/ExploreButton'; import ExploreButton from 'components/TableDetail/ExploreButton';
import Flag from 'components/common/Flag';
import FrequentUsers from 'components/TableDetail/FrequentUsers'; import FrequentUsers from 'components/TableDetail/FrequentUsers';
import LoadingSpinner from 'components/common/LoadingSpinner';
import LineageLink from 'components/TableDetail/LineageLink'; import LineageLink from 'components/TableDetail/LineageLink';
import OwnerEditor from 'components/TableDetail/OwnerEditor'; import OwnerEditor from 'components/TableDetail/OwnerEditor';
import SourceLink from 'components/TableDetail/SourceLink'; import SourceLink from 'components/TableDetail/SourceLink';
...@@ -37,32 +40,43 @@ import { getSourceIconClass, issueTrackingEnabled, notificationsEnabled } from ' ...@@ -37,32 +40,43 @@ import { getSourceIconClass, issueTrackingEnabled, notificationsEnabled } from '
import { formatDateTimeShort } from 'utils/dateUtils'; import { formatDateTimeShort } from 'utils/dateUtils';
import { getLoggingParams } from 'utils/logUtils'; import { getLoggingParams } from 'utils/logUtils';
import './styles';
import RequestDescriptionText from './RequestDescriptionText'; import RequestDescriptionText from './RequestDescriptionText';
import RequestMetadataForm from './RequestMetadataForm'; import RequestMetadataForm from './RequestMetadataForm';
import { PROGRMMATIC_DESC_HEADER } from './constants'; import { PROGRMMATIC_DESC_HEADER, ERROR_MESSAGE } from './constants';
import './styles.scss';
const SERVER_ERROR_CODE = 500;
const DASHBOARDS_PER_PAGE = 10;
const TABLE_SOURCE = "table_page";
export interface StateFromProps { export interface StateFromProps {
isLoading: boolean; isLoading: boolean;
statusCode?: number; statusCode?: number;
tableData: TableMetadata; tableData: TableMetadata;
} }
export interface DispatchFromProps { export interface DispatchFromProps {
getTableData: (key: string, searchIndex?: string, source?: string, ) => GetTableDataRequest; getTableData: (key: string, searchIndex?: string, source?: string, ) => GetTableDataRequest;
} }
export interface MatchProps {
interface MatchProps {
cluster: string; cluster: string;
database: string; database: string;
schema: string; schema: string;
table: string; table: string;
} }
export type TableDetailProps = StateFromProps & DispatchFromProps & RouteComponentProps<MatchProps>;
type TableDetailProps = StateFromProps & DispatchFromProps & RouteComponentProps<MatchProps>; const ErrorMessage = () => {
return (
<div className="container error-label">
<Breadcrumb />
<label>{ERROR_MESSAGE}</label>
</div>
);
};
class TableDetail extends React.Component<TableDetailProps & RouteComponentProps<any>> { export class TableDetail extends React.Component<TableDetailProps & RouteComponentProps<any>> {
private key: string; private key: string;
private didComponentMount: boolean = false; private didComponentMount: boolean = false;
...@@ -86,6 +100,7 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps ...@@ -86,6 +100,7 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps
getDisplayName() { getDisplayName() {
const params = this.props.match.params; const params = this.props.match.params;
return `${params.schema}.${params.table}`; return `${params.schema}.${params.table}`;
} }
...@@ -99,20 +114,47 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps ...@@ -99,20 +114,47 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps
return `${params.database}://${params.cluster}.${params.schema}/${params.table}`; return `${params.database}://${params.cluster}.${params.schema}/${params.table}`;
} }
renderTabs() {
const tabInfo = [];
// Default Column content
tabInfo.push({
content:
(
<ColumnList columns={ this.props.tableData.columns }/>
),
key: 'columns',
title: `Columns (${this.props.tableData.columns.length})`,
});
// Dashboard content
tabInfo.push({
content: (
<ResourceList
allItems={this.props.tableData.dashboards}
itemsPerPage={DASHBOARDS_PER_PAGE}
source={TABLE_SOURCE}
/>
),
key: "dashboards",
title: `Dashboards (${this.props.tableData.dashboards.length})`
});
return <TabsComponent tabs={ tabInfo } defaultTab={ "columns" } />;
}
render() { render() {
const {isLoading, statusCode, tableData} = this.props;
let innerContent; let innerContent;
// We want to avoid rendering the previous table's metadata before new data is fetched in componentDidMount // We want to avoid rendering the previous table's metadata before new data is fetched in componentDidMount
if (this.props.isLoading || !this.didComponentMount) { if (isLoading || !this.didComponentMount) {
innerContent = <LoadingSpinner/>; innerContent = <LoadingSpinner/>;
} else if (this.props.statusCode === 500) { } else if (statusCode === SERVER_ERROR_CODE) {
innerContent = ( innerContent = (<ErrorMessage />);
<div className="container error-label">
<Breadcrumb />
<label>Something went wrong...</label>
</div>
);
} else { } else {
const data = this.props.tableData; const data = tableData;
innerContent = ( innerContent = (
<div className="resource-detail-layout table-detail"> <div className="resource-detail-layout table-detail">
{ {
...@@ -228,7 +270,7 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps ...@@ -228,7 +270,7 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps
} }
</section> </section>
<section className="right-panel"> <section className="right-panel">
<ColumnList columns={ data.columns }/> { this.renderTabs() }
</section> </section>
</article> </article>
</div> </div>
...@@ -243,7 +285,6 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps ...@@ -243,7 +285,6 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps
} }
} }
export const mapStateToProps = (state: GlobalState) => { export const mapStateToProps = (state: GlobalState) => {
return { return {
isLoading: state.tableMetadata.isLoading, isLoading: state.tableMetadata.isLoading,
......
...@@ -54,4 +54,10 @@ ...@@ -54,4 +54,10 @@
} }
} }
} }
.right-panel & {
.tab-content .list-group-item:first-child {
border-top: none;
}
}
} }
...@@ -145,6 +145,7 @@ describe('generateExploreUrl', () => { ...@@ -145,6 +145,7 @@ describe('generateExploreUrl', () => {
badges: [], badges: [],
cluster: 'cluster', cluster: 'cluster',
columns: [], columns: [],
dashboards: [],
database: 'database', database: 'database',
is_editable: false, is_editable: false,
is_view: false, is_view: false,
......
import axios from 'axios'; import axios from 'axios';
import * as qs from 'simple-query-string'; import * as qs from 'simple-query-string';
import * as Helpers from '../helpers'; import * as Helpers from './helpers';
import * as Utils from 'ducks/utilMethods'; import * as Utils from 'ducks/utilMethods';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import {relatedDashboards} from 'fixtures/metadata/table';
import { NotificationType, UpdateMethod, UpdateOwnerPayload } from 'interfaces'; import { NotificationType, UpdateMethod, UpdateOwnerPayload } from 'interfaces';
import * as API from '../v0'; import * as API from './v0';
const filterFromObjSpy = jest.spyOn(Utils, 'filterFromObj').mockImplementation((initialObject, rejectedKeys) => { return initialObject; }); const filterFromObjSpy = jest.spyOn(Utils, 'filterFromObj');
jest.mock('axios'); jest.mock('axios');
describe('helpers', () => { describe('helpers', () => {
let mockResponseData: API.TableDataAPI; let mockResponseData: API.TableDataAPI;
let mockRelatedDashboardsResponseData: API.RelatedDashboardDataAPI;
let tableResponseData: API.TableData; let tableResponseData: API.TableData;
beforeAll(() => { beforeAll(() => {
tableResponseData = { tableResponseData = {
...@@ -24,6 +26,9 @@ describe('helpers', () => { ...@@ -24,6 +26,9 @@ describe('helpers', () => {
owners: [{display_name: 'test', profile_url: 'test.io', email: 'test@test.com', user_id: 'test'}], owners: [{display_name: 'test', profile_url: 'test.io', email: 'test@test.com', user_id: 'test'}],
tags: [{tag_count: 2, tag_name: 'zname'}, {tag_count: 1, tag_name: 'aname'}], tags: [{tag_count: 2, tag_name: 'zname'}, {tag_count: 1, tag_name: 'aname'}],
}; };
mockRelatedDashboardsResponseData = {
dashboards: relatedDashboards
};
mockResponseData = { mockResponseData = {
tableData: tableResponseData, tableData: tableResponseData,
msg: 'Success', msg: 'Success',
...@@ -54,11 +59,41 @@ describe('helpers', () => { ...@@ -54,11 +59,41 @@ describe('helpers', () => {
}); });
}); });
it('getTableDataFromResponseData', () => { describe('getRelatedDashboardSlug', () => {
Helpers.getTableDataFromResponseData(mockResponseData); it('generates related dashboard slug section for the URL', () => {
expect(filterFromObjSpy).toHaveBeenCalledWith(tableResponseData, ['owners', 'tags']); const tableKey = 'hive://gold.base/rides';
const actual = Helpers.getRelatedDashboardSlug(tableKey);
const expected = 'hive%3A%2F%2Fgold.base%2Frides';
expect(actual).toEqual(expected);
});
}); });
describe('getTableDataFromResponseData', () => {
it('uses the filterFromObj method', () => {
Helpers.getTableDataFromResponseData(mockResponseData, mockRelatedDashboardsResponseData);
expect(filterFromObjSpy).toHaveBeenCalledWith(tableResponseData, ['owners', 'tags']);
});
describe('produces the final TableMetadata information', () => {
it('contains the columns key', () => {
const expected = 0;
const actual = Helpers.getTableDataFromResponseData(mockResponseData, mockRelatedDashboardsResponseData).columns.length;
expect(actual).toEqual(expected);
});
it('contains the dashboards key', () => {
const expected = 3;
const actual = Helpers.getTableDataFromResponseData(mockResponseData, mockRelatedDashboardsResponseData).dashboards.length;
expect(actual).toEqual(expected);
});
});
});
it('getTableOwnersFromResponseData', () => { it('getTableOwnersFromResponseData', () => {
expect(Helpers.getTableOwnersFromResponseData(mockResponseData)).toEqual({ expect(Helpers.getTableOwnersFromResponseData(mockResponseData)).toEqual({
'test': {display_name: 'test', profile_url: 'test.io', email: 'test@test.com', user_id: 'test'} 'test': {display_name: 'test', profile_url: 'test.io', email: 'test@test.com', user_id: 'test'}
......
...@@ -13,10 +13,22 @@ export function getTableQueryParams(key: string, index?: string, source?: string ...@@ -13,10 +13,22 @@ export function getTableQueryParams(key: string, index?: string, source?: string
} }
/** /**
* Parses the response for table metadata to create a TableMetadata object * Generates the query string parameters needed for the request for the related dashboards to a table
*/ */
export function getTableDataFromResponseData(responseData: API.TableDataAPI): TableMetadata { export function getRelatedDashboardSlug(key: string): string {
return filterFromObj(responseData.tableData, ['owners', 'tags']) as TableMetadata; return encodeURIComponent(key);
}
/**
* Parses the response for table metadata and the related dashboard information to create a TableMetadata object
*/
export function getTableDataFromResponseData(responseData: API.TableDataAPI, relatedDashboardsData: API.RelatedDashboardDataAPI ): TableMetadata {
const mergedTableData = {
...filterFromObj(responseData.tableData, ['owners', 'tags']),
...filterFromObj(relatedDashboardsData, ['msg', 'status_code']),
};
return (mergedTableData) as TableMetadata;
} }
/** /**
......
import axios, { AxiosResponse, AxiosError } from 'axios'; import axios, { AxiosResponse, AxiosError } from 'axios';
import { PreviewData, PreviewQueryParams, TableMetadata, UpdateOwnerPayload, User, Tag } from 'interfaces'; import { PreviewData, PreviewQueryParams, TableMetadata, DashboardResource, UpdateOwnerPayload, User, Tag } from 'interfaces';
export const API_PATH = '/api/metadata/v0'; export const API_PATH = '/api/metadata/v0';
...@@ -14,26 +14,45 @@ export type TableData = TableMetadata & { ...@@ -14,26 +14,45 @@ export type TableData = TableMetadata & {
export type DescriptionAPI = { description: string; } & MessageAPI; export type DescriptionAPI = { description: string; } & MessageAPI;
export type LastIndexedAPI = { timestamp: string; } & MessageAPI; export type LastIndexedAPI = { timestamp: string; } & MessageAPI;
export type PreviewDataAPI = { previewData: PreviewData; } & MessageAPI; export type PreviewDataAPI = { previewData: PreviewData; } & MessageAPI;
export type TableDataAPI= { tableData: TableData; } & MessageAPI; export type TableDataAPI = { tableData: TableData; } & MessageAPI;
export type RelatedDashboardDataAPI = { dashboards: DashboardResource[]; };
/** HELPERS **/ /** HELPERS **/
import { import {
getTableQueryParams, getTableDataFromResponseData, getTableOwnersFromResponseData, getTableQueryParams, getRelatedDashboardSlug, getTableDataFromResponseData, getTableOwnersFromResponseData,
createOwnerUpdatePayload, createOwnerNotificationData, shouldSendNotification createOwnerUpdatePayload, createOwnerNotificationData, shouldSendNotification
} from './helpers'; } from './helpers';
export function getTableData(tableKey: string, index?: string, source?: string ) { export function getTableData(
const queryParams = getTableQueryParams(tableKey, index, source); tableKey: string,
return axios.get(`${API_PATH}/table?${queryParams}`) index?: string,
.then((response: AxiosResponse<TableDataAPI>) => { source?: string
return { ) {
data: getTableDataFromResponseData(response.data), const relatedDashboardsSlug: string = getRelatedDashboardSlug(tableKey);
owners: getTableOwnersFromResponseData(response.data), const relatedDashboardsURL: string = `${API_PATH}/table/${relatedDashboardsSlug}/dashboards`;
tags: response.data.tableData.tags, const relatedDashboardsRequest = axios.get<RelatedDashboardDataAPI>(
statusCode: response.status, relatedDashboardsURL
}; );
});
const tableQueryParams = getTableQueryParams(tableKey, index, source);
const tableURL = `${API_PATH}/table?${tableQueryParams}`;
const tableRequest = axios.get<TableDataAPI>(tableURL);
return Promise.all([tableRequest, relatedDashboardsRequest]).then(
([tableResponse, relatedDashboardsResponse]: [
AxiosResponse<TableDataAPI>,
AxiosResponse<RelatedDashboardDataAPI>
]) => ({
data: getTableDataFromResponseData(
tableResponse.data,
relatedDashboardsResponse.data
),
owners: getTableOwnersFromResponseData(tableResponse.data),
tags: tableResponse.data.tableData.tags,
statusCode: tableResponse.status
})
);
} }
export function getTableDescription(tableData: TableMetadata) { export function getTableDescription(tableData: TableMetadata) {
......
...@@ -178,6 +178,7 @@ export const initialTableDataState: TableMetadata = { ...@@ -178,6 +178,7 @@ export const initialTableDataState: TableMetadata = {
badges: [], badges: [],
cluster: '', cluster: '',
columns: [], columns: [],
dashboards: [],
database: '', database: '',
is_editable: false, is_editable: false,
is_view: false, is_view: false,
......
...@@ -153,6 +153,7 @@ const globalState: GlobalState = { ...@@ -153,6 +153,7 @@ const globalState: GlobalState = {
badges: [], badges: [],
cluster: '', cluster: '',
columns: [], columns: [],
dashboards: [],
database: '', database: '',
is_editable: false, is_editable: false,
is_view: false, is_view: false,
......
import { TableMetadata, DashboardResource, ResourceType } from '../../interfaces';
import { TagType } from '../../interfaces/Tags';
export const tableMetadata:TableMetadata = {
badges: [
{
tag_name: "ga",
tag_type: TagType.BADGE,
}
],
cluster: "gold",
columns: [
{
col_type: "bigint",
description: "Test Value",
is_editable: true,
name: "ride_id",
stats: [
{
end_epoch: 1588896000,
start_epoch: 1588896000,
stat_type: "count",
stat_val: "992487"
},
{
end_epoch: 1588896000,
start_epoch: 1588896000,
stat_type: "count_null",
stat_val: "0"
},
{
end_epoch: 1588896000,
start_epoch: 1588896000,
stat_type: "count_distinct",
stat_val: "992487"
}
]
},
{
col_type: "string",
description:
"ds will be the date part of requested_at ds will be the date part of requested_at ds will be the date part of requested_at ds will be the date part of requested_at ds will be the date part of requested_at ds will be the date part of requested_at ds w",
is_editable: true,
name: "ds",
stats: []
},
{
col_type: "string",
description: null,
is_editable: true,
name: "route_id",
stats: [
{
end_epoch: 1588896000,
start_epoch: 1588896000,
stat_type: "count",
stat_val: "992487"
},
{
end_epoch: 1588896000,
start_epoch: 1588896000,
stat_type: "count_null",
stat_val: "0"
},
{
end_epoch: 1588896000,
start_epoch: 1588896000,
stat_type: "count_distinct",
stat_val: "992405"
},
{
end_epoch: 1586476800,
start_epoch: 1586476800,
stat_type: "len_max",
stat_val: "24"
},
{
end_epoch: 1586476800,
start_epoch: 1586476800,
stat_type: "len_min",
stat_val: "24"
},
{
end_epoch: 1586476800,
start_epoch: 1586476800,
stat_type: "len_avg",
stat_val: "24"
}
]
}
],
dashboards: [],
database: "hive",
description:
"One row per ride request, showing all stages of the ride funnel. ",
is_editable: true,
is_view: false,
key: "hive://gold.base/rides",
last_updated_timestamp: 1583469780,
name: "rides",
partition: {
is_partitioned: true,
key: "ds",
value: "2020-03-05"
},
programmatic_descriptions: [],
schema: "base",
source: {
source:
"https://github.com/lyft/etl/blob/master/sql/hive/base/rides.config",
source_type: "github"
},
table_readers: [
{
read_count: 1735,
user: {
email: "testEmail@lyft.com",
user_id: "123test",
display_name: "Test User The First",
profile_url: "http://ProfileURLForTest.one"
}
},
{
read_count: 229,
user: {
email: "testEmailBis@lyft.com",
user_id: "456test",
display_name: "Test User The Second",
profile_url: "http://ProfileURLForTest.two"
}
},
{
read_count: 189,
user: {
email: "testEmailTri@lyft.com",
user_id: "789test",
display_name: "Test User The Third",
profile_url: "http://ProfileURLForTest.three"
}
}
],
table_writer: {
application_url:
"https://etl-production.lyft.net/admin/airflow/tree?dag_id=ADHOC - root",
description: "Airflow with id ADHOC - root/UNKNOWN",
id: "ADHOC - root/UNKNOWN",
name: "Airflow"
},
watermarks: [
{
create_time: "2020-02-13 19:55:13",
partition_key: "ds",
partition_value: "2020-03-05",
watermark_type: "high_watermark"
},
{
create_time: "2020-02-13 19:55:13",
partition_key: "ds",
partition_value: "2013-02-10",
watermark_type: "low_watermark"
}
]
};
export const relatedDashboards:DashboardResource[] = [
{
"group_name": "Test Group 1",
"description": null,
"cluster": "gold",
"group_url": "https://app.mode.com/testCompany/spaces/1234",
"uri": "mode_dashboard://gold.1234/23445asb",
"last_successful_run_timestamp": 1590505846,
"name": "Test Dashboard 1",
"product": "mode",
"type": ResourceType.dashboard,
"url": "https://app.mode.com/testCompany/reports/23445asb"
}, {
"group_name": "Test Group 2",
"description": null,
"cluster": "gold",
"group_url": "https://app.mode.com/testCompany/spaces/345asd",
"uri": "mode_dashboard://gold.345asd/asdfas001",
"last_successful_run_timestamp": 1590519704,
"name": "Test Dashboard 2",
"product": "mode",
"type": ResourceType.dashboard,
"url": "https://app.mode.com/testCompany/reports/asdfas001"
}, {
"group_name": "Test Group 3",
"description": null,
"cluster": "gold",
"group_url": "https://app.mode.com/testCompany/spaces/casdg80",
"uri": "mode_dashboard://gold.casdg80/123566",
"last_successful_run_timestamp": 1590538191,
"name": "Test Dashboard 3",
"product": "mode",
"type": ResourceType.dashboard,
"url": "https://app.mode.com/testCompany/reports/123566"
}
];
import { UpdateMethod } from './Enums'; import { UpdateMethod } from './Enums';
import { User } from './User'; import { User } from './User';
import { Badge } from './Tags'; import { Badge } from './Tags';
import { DashboardResource } from './Resources';
interface PartitionData { interface PartitionData {
is_partitioned: boolean; is_partitioned: boolean;
...@@ -78,6 +79,7 @@ export interface TableMetadata { ...@@ -78,6 +79,7 @@ export interface TableMetadata {
badges: Badge[]; badges: Badge[];
cluster: string; cluster: string;
columns: TableColumn[]; columns: TableColumn[];
dashboards: DashboardResource[];
database: string; database: string;
is_editable: boolean; is_editable: boolean;
is_view: boolean; is_view: boolean;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment