Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.texera.web.resource.dashboard

import org.apache.texera.web.resource.dashboard.FulltextSearchQueryUtils.{
getContainsFilter,
getDateFilter,
getFullTextSearchFilter
}
import org.apache.texera.dao.jooq.generated.tables.User.USER
import org.jooq.impl.DSL
import org.jooq.{Condition, GroupField, Record, TableLike}

import scala.jdk.CollectionConverters.CollectionHasAsScala

/**
* The one copy of FROM / WHERE / hydration for every LakeFS-backed resource. A concrete
* builder supplies only its [[VersionedResourceTables]] descriptor and its projection.
*/
abstract class VersionedResourceSearchQueryBuilder[Rec <: Record, P](
tables: VersionedResourceTables[Rec, P]
) extends SearchQueryBuilder {

/**
* `uid` is null for anonymous callers. Visibility: public only when `uid` is null;
* explicitly-granted only when `includePublic` is false; both when it is true.
*/
override protected def constructFromClause(
uid: Integer,
params: DashboardResource.SearchQueryParams,
includePublic: Boolean = false
): TableLike[_] = {
val baseJoin = tables.joinWithAccessAndOwner(
Some(if (uid == null) DSL.falseCondition() else tables.access.uidColumn.eq(uid))
)

val condition: Condition =
if (uid == null) {
tables.isPublicColumn.eq(true)
} else {
if (includePublic) {
tables.isPublicColumn.eq(true).or(tables.access.uidColumn.isNotNull)
} else {
tables.access.uidColumn.isNotNull
}
}
baseJoin.where(condition)
}

override protected def constructWhereClause(
uid: Integer,
params: DashboardResource.SearchQueryParams
): Condition = {
val splitKeywords = params.keywords.asScala
.flatMap(_.split("[+\\-()<>~*@\"]"))
.filter(_.nonEmpty)
.toSeq

getDateFilter(
params.creationStartDate,
params.creationEndDate,
tables.creationTimeColumn
)
.and(getContainsFilter(tables.searchIds(params), tables.idColumn))
.and(getContainsFilter(params.owners, USER.EMAIL))
.and(
getFullTextSearchFilter(splitKeywords, List(tables.nameColumn, tables.descriptionColumn))
)
}

override protected def getGroupByFields: Seq[GroupField] = {
Seq.empty
}

override protected def toEntryImpl(
uid: Integer,
record: Record
): DashboardResource.DashboardClickableFileEntry =
// null = mismatch; searchAllResources drops it and flips hasMismatch.
tables.hydrate(record, uid).map(_._2).orNull
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
</nz-dropdown-menu>

<a
*ngIf="this.isLogin"
*ngIf="this.isLogin && hasIdFilter"
[nzDropdownMenu]="IDSearchOptions"
nz-dropdown
nzTrigger="click"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ import { HttpClientTestingModule } from "@angular/common/http/testing";
import { commonTestProviders } from "src/app/common/testing/test-utils";
import { NzModalModule } from "ng-zorro-antd/modal";
import { en_US, provideNzI18n } from "ng-zorro-antd/i18n";
<<<<<<< HEAD
=======
import { UserService } from "src/app/common/service/user/user.service";
import { MOCK_USER, StubUserService } from "src/app/common/service/user/stub-user.service";
import { UserProjectService } from "src/app/dashboard/service/user/project/user-project.service";
import { StubUserProjectService } from "src/app/dashboard/service/user/project/stub-user-project.service";
import { NotificationService } from "src/app/common/service/notification/notification.service";
import { DatasetService } from "src/app/dashboard/service/user/dataset/dataset.service";
import { EntityType } from "src/app/hub/service/hub.service";
import { By } from "@angular/platform-browser";
import { of } from "rxjs";
>>>>>>> eddec267a (fix(frontend, amber): source filter owners per resource kind and apply them to datasets (#8060))

describe("FiltersComponent", () => {
let component: FiltersComponent;
Expand All @@ -44,6 +56,12 @@ describe("FiltersComponent", () => {
{ provide: JWT_OPTIONS, useValue: {} },
{ provide: WorkflowPersistService, useValue: new StubWorkflowPersistService(testWorkflowEntries) },
{ provide: OperatorMetadataService, useClass: StubOperatorMetadataService },
<<<<<<< HEAD
=======
{ provide: UserService, useClass: StubUserService },
{ provide: UserProjectService, useClass: StubUserProjectService },
{ provide: DatasetService, useValue: { retrieveOwners: vi.fn(() => of([])) } },
>>>>>>> eddec267a (fix(frontend, amber): source filter owners per resource kind and apply them to datasets (#8060))
provideNzI18n(en_US),
...commonTestProviders,
],
Expand Down Expand Up @@ -85,3 +103,95 @@ describe("FiltersComponent", () => {
]);
});
});

/** The bar is shared by several pages; these pin that it sources owners and ids per kind. */
describe("FiltersComponent per-resource owners", () => {
let fixture: ComponentFixture<FiltersComponent>;
let component: FiltersComponent;
let datasetOwners: ReturnType<typeof vi.fn>;
let workflowOwners: ReturnType<typeof vi.fn>;
let workflowIds: ReturnType<typeof vi.fn>;

/** The input has to be set before ngOnInit reads it. */
async function render(entityType?: EntityType): Promise<void> {
datasetOwners = vi.fn(() => of(["dataset-owner"]));
workflowOwners = vi.fn(() => of(["workflow-owner"]));
workflowIds = vi.fn(() => of([7]));

await TestBed.configureTestingModule({
providers: [
JwtHelperService,
{ provide: JWT_OPTIONS, useValue: {} },
{
provide: WorkflowPersistService,
useValue: { retrieveOwners: workflowOwners, retrieveWorkflowIDs: workflowIds },
},
{ provide: DatasetService, useValue: { retrieveOwners: datasetOwners } },
{ provide: OperatorMetadataService, useClass: StubOperatorMetadataService },
{ provide: UserService, useClass: StubUserService },
{ provide: UserProjectService, useClass: StubUserProjectService },
provideNzI18n(en_US),
...commonTestProviders,
],
imports: [FiltersComponent, NzModalModule, NzDropDownModule, FormsModule, HttpClientTestingModule],
}).compileComponents();

fixture = TestBed.createComponent(FiltersComponent);
component = fixture.componentInstance;
if (entityType !== undefined) {
component.entityType = entityType;
}
fixture.detectChanges();
}

afterEach(() => {
const overlayContainer = TestBed.inject(OverlayContainer, null);
if (overlayContainer) {
overlayContainer.getContainerElement().innerHTML = "";
}
});

it("defaults to workflows, so the call sites that pass nothing are unaffected", async () => {
await render();

expect(component.entityType).toBe(EntityType.Workflow);
expect(workflowOwners).toHaveBeenCalled();
expect(datasetOwners).not.toHaveBeenCalled();
expect(component.owners.map(owner => owner.userName)).toEqual(["workflow-owner"]);
});

it("lists dataset owners, not workflow owners, when filtering datasets", async () => {
await render(EntityType.Dataset);

expect(datasetOwners).toHaveBeenCalled();
expect(workflowOwners).not.toHaveBeenCalled();
expect(component.owners.map(owner => owner.userName)).toEqual(["dataset-owner"]);
});

it("offers workflow ids only when filtering workflows", async () => {
await render(EntityType.Workflow);
expect(component.hasIdFilter).toBe(true);
expect(workflowIds).toHaveBeenCalled();
expect(component.wids.map(wid => wid.id)).toEqual(["7"]);
});

it("asks for no ids at all when filtering datasets, rather than showing workflow ids", async () => {
await render(EntityType.Dataset);

expect(component.hasIdFilter).toBe(false);
expect(workflowIds).not.toHaveBeenCalled();
expect(component.wids).toEqual([]);
});

it("hides the id dropdown for a kind that has no ids to offer", async () => {
await render(EntityType.Dataset);

expect(fixture.debugElement.query(By.css(".search-wids-button"))).toBeNull();
});

it("still renders the id dropdown for workflows", async () => {
await render(EntityType.Workflow);

expect(fixture.debugElement.query(By.css(".search-wids-button"))).not.toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { DashboardProject } from "../../../type/dashboard-project.interface";
import { NotificationService } from "src/app/common/service/notification/notification.service";
import { UserProjectService } from "../../../service/user/project/user-project.service";
import { WorkflowPersistService } from "src/app/common/service/workflow-persist/workflow-persist.service";
import { DatasetService } from "../../../service/user/dataset/dataset.service";
import { EntityType } from "../../../../hub/service/hub.service";
import { SearchFilterParameters } from "../../../type/search-filter-parameters";
import { UserService } from "../../../../common/service/user/user.service";
import { switchMap } from "rxjs/operators";
Expand Down Expand Up @@ -71,6 +73,8 @@ export class FiltersComponent implements OnInit {
private _masterFilterList: ReadonlyArray<string> = [];
// receive input from parent components (UserProjectSection), if any
@Input() public pid?: number = undefined;
/** Which resource kind this page lists; decides whose owners and ids are offered. */
@Input() public entityType: EntityType = EntityType.Workflow;
@Output()
public masterFilterListChange = new EventEmitter<typeof this._masterFilterList>();
public get masterFilterList(): ReadonlyArray<string> {
Expand Down Expand Up @@ -121,9 +125,21 @@ export class FiltersComponent implements OnInit {
private notificationService: NotificationService,
private userProjectService: UserProjectService,
private workflowPersistService: WorkflowPersistService,
private datasetService: DatasetService,
private cdr: ChangeDetectorRef
) {}

/** Only workflows expose an id-listing endpoint. */
public get hasIdFilter(): boolean {
return this.entityType === EntityType.Workflow;
}

private retrieveOwners(): Observable<string[]> {
return this.entityType === EntityType.Dataset
? this.datasetService.retrieveOwners()
: this.workflowPersistService.retrieveOwners();
}

ngOnInit(): void {
this.setupUserProject();
this.searchParameterBackendSetup();
Expand Down Expand Up @@ -157,9 +173,7 @@ export class FiltersComponent implements OnInit {
});
}

/**
* Backend calls for Workflow IDs, Owners, and Operators in saved workflow component
*/
/** Backend calls for the filtered kind's owners and ids, plus the operator metadata. */
private searchParameterBackendSetup() {
this.operatorMetadataService
.getOperatorMetadata()
Expand All @@ -183,23 +197,24 @@ export class FiltersComponent implements OnInit {
this.operatorGroups = opdata.groups.map(group => group.groupName);
});
if (this.isLogin) {
this.workflowPersistService
.retrieveOwners()
this.retrieveOwners()
.pipe(untilDestroyed(this))
.subscribe(list_of_owners => {
this.owners = list_of_owners.map(i => ({ userName: i, checked: false }));
});
this.workflowPersistService
.retrieveWorkflowIDs()
.pipe(untilDestroyed(this))
.subscribe(wids => {
this.wids = wids.map(wid => {
return {
id: wid.toString(),
checked: false,
};
if (this.hasIdFilter) {
this.workflowPersistService
.retrieveWorkflowIDs()
.pipe(untilDestroyed(this))
.subscribe(wids => {
this.wids = wids.map(wid => {
return {
id: wid.toString(),
checked: false,
};
});
});
});
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,39 @@ <h2 class="page-title">Datasets</h2>
nzTheme="outline"></i>
<span>Create Dataset</span>
</button>
<<<<<<< HEAD
<texera-filters #filters></texera-filters>
=======
<nz-space-compact class="utility-button-group">
<texera-sort-button
[showEditTime]="false"
[showExecutionTime]="false"
(sortMethodChange)="sortMethod = $event; search()"></texera-sort-button>
<button
nz-button
title="List View"
(click)="setViewType('list')"
[nzType]="viewType === 'list' ? 'primary' : 'default'">
<i
nz-icon
nzType="bars"
nzTheme="outline"></i>
</button>
<button
nz-button
title="Card View"
(click)="setViewType('card')"
[nzType]="viewType === 'card' ? 'primary' : 'default'">
<i
nz-icon
nzType="appstore"
nzTheme="outline"></i>
</button>
</nz-space-compact>
<texera-filters
[entityType]="entityType"
#filters></texera-filters>
>>>>>>> eddec267a (fix(frontend, amber): source filter owners per resource kind and apply them to datasets (#8060))
</div>
</nz-card>

Expand Down
Loading