Skip to content

Commit 6242c23

Browse files
committed
[frontend] Add iceberg icon to tables in left assist popover
Info about iceberg is at the time only available when calling describe per table so the iceberg icon is not showing in the initial table list.
1 parent 6ba92cc commit 6242c23

File tree

7 files changed

+140
-12
lines changed

7 files changed

+140
-12
lines changed

desktop/core/src/desktop/js/catalog/DataCatalogEntry.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export interface NavigatorMeta extends TimestampedData {
180180
}
181181

182182
export interface TableAnalysis extends TimestampedData {
183-
cols: { comment?: string | null; type: string; name: string };
183+
cols: { comment?: string | null; type: string; name: string }[];
184184
comment?: string | null;
185185
details: {
186186
properties: { [propertyKey: string]: string };
@@ -1414,6 +1414,13 @@ export default class DataCatalogEntry {
14141414
return false;
14151415
}
14161416

1417+
/**
1418+
* Returns true if the entry is an Iceberg table
1419+
*/
1420+
isIcebergTable(): boolean {
1421+
return this.analysis?.details?.stats?.table_type === 'ICEBERG';
1422+
}
1423+
14171424
/**
14181425
* Returns true if the entry is a view. It will be accurate once the source meta has been loaded.
14191426
*/

desktop/core/src/desktop/js/catalog/dataCatalogEntry.test.ts

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import { CancellablePromise } from 'api/cancellablePromise';
1818
import dataCatalog from 'catalog/dataCatalog';
19-
import DataCatalogEntry, { Sample } from 'catalog/DataCatalogEntry';
19+
import DataCatalogEntry, { Sample, TableAnalysis } from 'catalog/DataCatalogEntry';
2020
import { Compute, Connector, Namespace } from 'config/types';
2121
import * as CatalogApi from './api';
2222

@@ -60,6 +60,95 @@ describe('dataCatalogEntry.ts', () => {
6060

6161
afterAll(clearStorage);
6262

63+
describe('getAnalysis', () => {
64+
const emptyAnalysisApiSpy = () => {
65+
jest.spyOn(CatalogApi, 'fetchDescribe').mockReturnValue(
66+
CancellablePromise.resolve<TableAnalysis>({
67+
message: '',
68+
name: 'iceberg-table-test',
69+
partition_keys: [],
70+
cols: [{ name: 'i', type: 'int', comment: '' }],
71+
path_location: 'test',
72+
hdfs_link: '/test',
73+
is_view: false,
74+
properties: [],
75+
details: {
76+
stats: {
77+
table_type: 'ICEBERG'
78+
},
79+
properties: {}
80+
},
81+
stats: [],
82+
primary_keys: []
83+
})
84+
);
85+
};
86+
87+
it('should return true for isIcebergTable after the analysis has has been loaded', async () => {
88+
emptyAnalysisApiSpy();
89+
const entry = await getEntry('someDb.someIcebergTable');
90+
91+
expect(entry.isIcebergTable()).toBeFalsy();
92+
93+
await entry.getAnalysis();
94+
expect(entry.isIcebergTable()).toBeTruthy();
95+
});
96+
97+
it('rejects a cachedOnly request if there is no previous promise', async () => {
98+
emptyAnalysisApiSpy();
99+
const entryA = await getEntry('someDb.someTable');
100+
let rejected = false;
101+
await entryA.getAnalysis({ cachedOnly: true }).catch(() => {
102+
rejected = true;
103+
});
104+
105+
expect(rejected).toBeTruthy();
106+
});
107+
108+
it('should return the same analysis promise for the same entry', async () => {
109+
emptyAnalysisApiSpy();
110+
const entryA = await getEntry('someDb.someTable');
111+
const entryB = await getEntry('someDb.someTable');
112+
expect(entryA.getAnalysis()).toEqual(entryB.getAnalysis());
113+
});
114+
115+
it('should not return the same analysis promise for different entries', async () => {
116+
emptyAnalysisApiSpy();
117+
const entryA = await getEntry('someDb.someTableOne');
118+
const entryB = await getEntry('someDb.someTableTwo');
119+
expect(entryA.getAnalysis()).not.toEqual(entryB.getAnalysis());
120+
});
121+
122+
it('should keep the analysis promise for future session use', async () => {
123+
emptyAnalysisApiSpy();
124+
const entryA = await getEntry('someDb.someTable');
125+
await entryA.clearCache();
126+
const analysisPromise = entryA.getAnalysis();
127+
expect(entryA.analysisPromise).toEqual(analysisPromise);
128+
const entryB = await getEntry('someDb.someTable');
129+
expect(entryB.analysisPromise).toEqual(analysisPromise);
130+
});
131+
132+
it('should not cancel when cancellable option is not set to true', async () => {
133+
emptyAnalysisApiSpy();
134+
const entryA = await getEntry('someDb.someTable');
135+
const analysisPromise = entryA.getAnalysis({ cancellable: false });
136+
await analysisPromise.cancel();
137+
expect(analysisPromise.cancelled).toBeFalsy();
138+
expect(entryA.analysisPromise).toEqual(analysisPromise);
139+
});
140+
141+
it('should not return a cancelled analysis promise', async () => {
142+
emptyAnalysisApiSpy();
143+
const entryA = await getEntry('someDb.someTable');
144+
const cancelledPromise = entryA.getAnalysis({ cancellable: true });
145+
await cancelledPromise.cancel();
146+
const newPromise = entryA.getAnalysis();
147+
expect(cancelledPromise.cancelled).toBeTruthy();
148+
expect(newPromise).not.toEqual(cancelledPromise);
149+
});
150+
});
151+
63152
describe('getSample', () => {
64153
const emptySampleApiSpy = () => {
65154
jest.spyOn(CatalogApi, 'fetchSample').mockReturnValue(

desktop/core/src/desktop/js/ko/components/contextPopover/dataCatalogContext.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,6 @@ class DataCatalogContext {
124124
.catch(() => {
125125
self.hasErrors(true);
126126
})
127-
.finally(() => {
128-
self.loading(false);
129-
})
130127
);
131128

132129
// TODO: Use connector attributes in dataCatalogContext
@@ -177,6 +174,7 @@ class DataCatalogContext {
177174
);
178175

179176
$.when.apply($, self.activePromises).always(() => {
177+
self.loading(false);
180178
self.activePromises.length = 0;
181179
});
182180
}

desktop/core/src/desktop/js/ko/components/contextPopover/ko.contextPopover.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,31 @@ const SUPPORT_TEMPLATES = `
276276
277277
<script type="text/html" id="context-catalog-entry-title">
278278
<div class="hue-popover-title">
279-
<i class="hue-popover-title-icon fa muted" data-bind="css: catalogEntry() && (catalogEntry().isView() || parentIsView()) ? 'fa-eye' : (catalogEntry().isDatabase() ? 'fa-database' : (catalogEntry().isModel() ? 'fa-puzzle-piece' : 'fa-table'))"></i>
279+
<!-- ko if: catalogEntry().isTable() -->
280+
<span class="hue-popover-title-secondary-icon-container">
281+
<!-- ko hueSpinner: { spin: loading, inline: true } --><!-- /ko -->
282+
<!-- ko ifnot: loading -->
283+
<!-- ko if:catalogEntry().isIcebergTable() -->
284+
<i class="hue-popover-title-icon fa muted fa-snowflake-o" title="${I18n(
285+
'Iceberg table'
286+
)}"></i>
287+
<!-- /ko -->
288+
<!-- ko ifnot:catalogEntry().isIcebergTable() -->
289+
<i class="hue-popover-title-icon fa muted fa-table"></i>
290+
<!-- /ko -->
291+
<!-- /ko -->
292+
</span>
293+
<!-- /ko -->
294+
<!-- ko ifnot: catalogEntry().isTable() -->
295+
<i class="hue-popover-title-icon fa muted" data-bind="css:
296+
catalogEntry() && (catalogEntry().isView() || parentIsView())
297+
? 'fa-eye'
298+
: (catalogEntry().isDatabase()
299+
? 'fa-database'
300+
: (catalogEntry().isModel()
301+
? 'fa-puzzle-piece'
302+
: 'fa-table'))"></i>
303+
<!-- /ko -->
280304
<span class="hue-popover-title-text" data-bind="foreach: breadCrumbs">
281305
<!-- ko ifnot: isActive --><div><a href="javascript: void(0);" data-bind="click: makeActive, text: name"></a>.</div><!-- /ko -->
282306
<!-- ko if: isActive -->

desktop/core/src/desktop/static/desktop/css/hue.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/core/src/desktop/static/desktop/css/hue3-extra.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/core/src/desktop/static/desktop/less/components/hue-popover.less

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
.hue-popover.hue-popover-top .hue-popover-arrow::after {
5353
bottom: 1px;
5454
margin-left: -3px;
55-
content: "";
55+
content: '';
5656
border-top-color: @hue-primary-color-dark;
5757
border-bottom-width: 0;
5858
}
@@ -72,7 +72,7 @@
7272
.hue-popover.hue-popover-right .hue-popover-arrow::after {
7373
bottom: -3px;
7474
left: 1px;
75-
content: "";
75+
content: '';
7676
border-right-color: @hue-primary-color-dark;
7777
border-left-width: 0;
7878
}
@@ -92,7 +92,7 @@
9292
.hue-popover.hue-popover-bottom .hue-popover-arrow::after {
9393
top: 3px;
9494
margin-left: -3px;
95-
content: "";
95+
content: '';
9696
border-top-width: 0;
9797
border-bottom-color: @hue-primary-color-dark;
9898
}
@@ -112,7 +112,7 @@
112112
.hue-popover.hue-popover-left .hue-popover-arrow::after {
113113
right: 2px;
114114
bottom: -3px;
115-
content: "";
115+
content: '';
116116
border-right-width: 0;
117117
border-left-color: @hue-primary-color-dark;
118118
}
@@ -131,6 +131,16 @@
131131
margin-top: 3px;
132132
}
133133

134+
.hue-popover-title-secondary-icon-container {
135+
width: 20px;
136+
display: inline-block;
137+
margin-top: 3px;
138+
139+
.hue-popover-title-icon {
140+
margin-top: 0;
141+
}
142+
}
143+
134144
.hue-popover-title-text {
135145
padding-left: 4px;
136146
font-size: 0;

0 commit comments

Comments
 (0)