Skip to content

Commit 2a9fb19

Browse files
authored
Merge pull request #2837 from cloudera/bjorn--Left-Assists-should-have-an-icon-to-indicate-Iceberg-Tables
[frontend] Add iceberg icon to tables in left assist popover
2 parents 6ba92cc + 6242c23 commit 2a9fb19

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)