Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dist/5036.7005e959b2f3fa80.js

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion dist/assets/i18n/dataExplorer/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@
"recordDetail": "Record Detail",
"nullValue": "NULL",
"showApiCall": "Show API Call",
"quickSearch": "Search rows..."
"quickSearch": "Search rows...",
"searchDatabases": "Search databases",
"noMatches": "No databases match"
}
2 changes: 1 addition & 1 deletion dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
<body class="mat-typography">
<df-root></df-root>
<script type="text/javascript" src="https://assets.calendly.com/assets/external/widget.js"></script>
<script src="runtime.70b3ff8deb75852a.js" type="module"></script><script src="polyfills.acd6a468b9e08280.js" type="module"></script><script src="main.a8aea8bd0d10987a.js" type="module"></script></body>
<script src="runtime.717f63c7399a7721.js" type="module"></script><script src="polyfills.acd6a468b9e08280.js" type="module"></script><script src="main.a8aea8bd0d10987a.js" type="module"></script></body>
</html>
1 change: 1 addition & 0 deletions dist/runtime.717f63c7399a7721.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/app/adf-data-explorer/df-data-explorer.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<ng-container *ngIf="selectedDb">
<df-schema-tree
[serviceName]="selectedDb.name"
[serviceLabel]="selectedDb.label || selectedDb.name"
[serviceLabel]="selectedDb.name"
[tables]="tables"
[loading]="loadingSchema"
[error]="errorSchema"
Expand Down
90 changes: 86 additions & 4 deletions src/app/adf-data-explorer/df-db-selector.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,24 +55,41 @@ import { DatabaseService } from './services/data-explorer.service';
<small>{{ t('dataExplorer.noDatabasesHint') }}</small>
</div>

<!-- Search: only worth the space once the list is long -->
<div
class="db-search"
*ngIf="!loading && !error && databases.length > searchThreshold">
<mat-icon class="db-search-icon">search</mat-icon>
<input
type="search"
[placeholder]="t('dataExplorer.searchDatabases')"
[attr.aria-label]="t('dataExplorer.searchDatabases')"
(input)="onSearch($event)" />
</div>

<!-- Database list -->
<mat-nav-list
*ngIf="!loading && !error && databases.length > 0"
class="db-list">
<a
mat-list-item
*ngFor="let db of databases; trackBy: trackByDbName"
*ngFor="let db of filtered; trackBy: trackByDbName"
tabindex="0"
(click)="databaseSelected.emit(db)"
[matTooltip]="db.description || db.name"
(keyup.enter)="databaseSelected.emit(db)"
[matTooltip]="tooltipFor(db)"
matTooltipPosition="right"
class="db-item">
<fa-icon [icon]="faDatabase" class="db-icon"></fa-icon>
<div class="db-info">
<span class="db-name">{{ db.label || db.name }}</span>
<span class="db-type">{{ db.type }}</span>
<span class="db-name">{{ db.name }}</span>
<span class="db-type">{{ db.typeLabel || db.type }}</span>
</div>
<mat-icon class="chevron">chevron_right</mat-icon>
</a>
<div class="no-matches" *ngIf="filtered.length === 0">
{{ t('dataExplorer.noMatches') }}
</div>
</mat-nav-list>
</div>
`,
Expand Down Expand Up @@ -118,12 +135,49 @@ import { DatabaseService } from './services/data-explorer.service';
font-size: 1.3rem;
}

.db-search {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-bottom: 1px solid var(--df-border);

.db-search-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--df-text-muted);
}

input {
flex: 1;
min-width: 0;
border: 0;
background: transparent;
color: var(--df-text);
font: inherit;
font-size: 1.3rem;
outline: none;

&::placeholder {
color: var(--df-text-muted);
}
}
}

.db-list {
flex: 1;
overflow-y: auto;
padding-top: 4px;
}

.no-matches {
padding: 24px 16px;
text-align: center;
color: var(--df-text-muted);
font-size: 1.3rem;
}

.db-item {
height: 56px !important;
padding: 0 16px !important;
Expand Down Expand Up @@ -190,6 +244,34 @@ export class DfDbSelectorComponent {

faDatabase = faDatabase;

/** Below this many services a search box is just noise. */
readonly searchThreshold = 8;
query = '';

get filtered(): DatabaseService[] {
const q = this.query.trim().toLowerCase();
if (!q) {
return this.databases;
}
return this.databases.filter(db =>
[db.name, db.label, db.typeLabel, db.type].some(v =>
(v ?? '').toLowerCase().includes(q)
)
);
}

onSearch(event: Event): void {
this.query = (event.target as HTMLInputElement).value;
}

/** The hover text carries what the row does not: description, then a label that differs from the name. */
tooltipFor(db: DatabaseService): string {
if (db.description) {
return db.description;
}
return db.label && db.label !== db.name ? db.label : db.name;
}

trackByDbName(_index: number, db: DatabaseService): string {
return db.name;
}
Expand Down
32 changes: 21 additions & 11 deletions src/app/adf-data-explorer/services/data-explorer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ export interface DatabaseService {
label: string;
description: string;
type: string;
/** Human name of the service type, e.g. "MySQL" for type "mysql". */
typeLabel?: string;
isActive: boolean;
}

export interface ServiceType {
name: string;
label?: string;
}

export interface TableInfo {
Expand Down Expand Up @@ -107,36 +110,39 @@ export class DataExplorerService {
private http = inject(HttpClient);

getDatabaseServices(): Observable<DatabaseService[]> {
// Step 1: Get service type names for Database and Big Data groups
// Step 1: service types in the Database and Big Data groups. The label
// ("MySQL", "PostgreSQL") is what the selector shows under the service
// name; the raw type name is only used to filter services.
const typeRequests = DB_GROUPS.map(group =>
this.http.get<{ resource: ServiceType[] }>(
`${BASE_URL}/system/service_type`,
{
params: { fields: 'name', group },
params: { fields: 'name,label', group },
headers: { 'show-loading': '', 'Cache-Control': 'no-cache, private' },
context: toastOff(),
}
)
);

return forkJoin(typeRequests).pipe(
map(responses =>
responses.flatMap(r => r.resource || []).map(t => t.name)
),
switchMap(typeNames => {
if (typeNames.length === 0) {
map(responses => responses.flatMap(r => r.resource || [])),
switchMap(types => {
if (types.length === 0) {
return new Observable<DatabaseService[]>(sub => {
sub.next([]);
sub.complete();
});
}
const typeFilter = `(type in ("${typeNames.join('","')}"))`;
const labelByType = new Map(types.map(t => [t.name, t.label]));
const typeFilter = `(type in ("${types.map(t => t.name).join('","')}"))`;
// Step 2: every active database service. No client-side row cap: an
// instance with hundreds of connections must list all of them, so
// leave the page size to the server's max_records_returned.
return this.http
.get<{ resource: DatabaseService[] }>(`${BASE_URL}/system/service`, {
params: {
filter: typeFilter,
fields: 'id,name,label,description,type',
limit: '100',
fields: 'id,name,label,description,type,is_active',
sort: 'name',
},
headers: {
Expand All @@ -146,7 +152,11 @@ export class DataExplorerService {
context: toastOff(),
})
.pipe(
map(res => (res.resource || []).filter(s => s.isActive !== false))
map(res =>
(res.resource || [])
.filter(s => s.isActive !== false)
.map(s => ({ ...s, typeLabel: labelByType.get(s.type) }))
)
);
})
);
Expand Down
4 changes: 3 additions & 1 deletion src/assets/i18n/dataExplorer/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@
"recordDetail": "Record Detail",
"nullValue": "NULL",
"showApiCall": "Show API Call",
"quickSearch": "Search rows..."
"quickSearch": "Search rows...",
"searchDatabases": "Search databases",
"noMatches": "No databases match"
}
Loading