diff --git a/.licenserc.yaml b/.licenserc.yaml index a9fcde7e3b9..fc97bf09f19 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -52,6 +52,8 @@ header: - 'frontend/src/assets/svg/operator-reuse-cache-invalid.svg' - 'frontend/src/assets/svg/operator-reuse-cache-valid.svg' - 'frontend/src/assets/notebook_migration_tool/jupyter-logo.svg' + # Third-party icon glyph - see LICENSE file for attribution + - 'frontend/src/app/common/icon/model-icon.ts' - 'frontend/src/app/common/type/proto/org/apache/texera/amber/core/virtualidentity.ts' - 'frontend/src/app/common/type/proto/org/apache/texera/amber/core/workflow.ts' - 'frontend/src/app/common/type/proto/google/protobuf/descriptor.ts' diff --git a/LICENSE b/LICENSE index b73a5017149..9e1e228eb23 100644 --- a/LICENSE +++ b/LICENSE @@ -232,6 +232,12 @@ This product bundles code derived from TypeFox monaco-languageclient: Source: https://github.com/TypeFox/monaco-languageclient License: MIT License (licenses-3rd-party-code/monaco-languageclient.txt) +This product includes an icon from Google's Material Symbols: + - frontend/src/app/common/icon/model-icon.ts ("hub") + Copyright (c) Google LLC + Source: https://github.com/google/material-design-icons + License: Apache License 2.0 (this LICENSE file) + This product includes SVG icons from SVGRepo: - frontend/src/assets/svg/operator-view-result.svg - frontend/src/assets/svg/operator-reuse-cache-valid.svg diff --git a/file-service/src/main/scala/org/apache/texera/service/resource/ModelResource.scala b/file-service/src/main/scala/org/apache/texera/service/resource/ModelResource.scala index 54cf7cc5ce8..80a69cbda2a 100644 --- a/file-service/src/main/scala/org/apache/texera/service/resource/ModelResource.scala +++ b/file-service/src/main/scala/org/apache/texera/service/resource/ModelResource.scala @@ -501,7 +501,8 @@ class ModelResource extends LazyLogging { MODEL_RESOURCE, uid, classOf[Model], - (model: Model) => model.getMid + (model: Model) => model.getMid, + includePublic = false )( fromGrant = (model, ownerEmail, privilege, isOwner) => Some( diff --git a/file-service/src/main/scala/org/apache/texera/service/resource/ResourceAccess.scala b/file-service/src/main/scala/org/apache/texera/service/resource/ResourceAccess.scala index 3276d32b68d..b12204877d7 100644 --- a/file-service/src/main/scala/org/apache/texera/service/resource/ResourceAccess.scala +++ b/file-service/src/main/scala/org/apache/texera/service/resource/ResourceAccess.scala @@ -187,7 +187,8 @@ object ResourceAccess { resource: ResourceTables[R, A], uid: Integer, pojoClass: Class[P], - idOf: P => Integer + idOf: P => Integer, + includePublic: Boolean = true )( fromGrant: (P, String, PrivilegeEnum, Boolean) => Option[D], fromPublic: (P, String) => Option[D] @@ -216,22 +217,25 @@ object ResourceAccess { val grantedIds = granted.map(_._1).toSet - val public = ctx - .select() - .from( - resource.table - .leftJoin(USER) - .on(USER.UID.eq(resource.ownerUidField)) - ) - .where(resource.isPublicField.eq(true)) - .fetch() - .asScala - .toList - .flatMap { record => - val entity = record.into(resource.table).into(pojoClass) - if (grantedIds.contains(idOf(entity))) None - else fromPublic(entity, record.into(USER).getEmail) - } + val public = + if (!includePublic) Nil + else + ctx + .select() + .from( + resource.table + .leftJoin(USER) + .on(USER.UID.eq(resource.ownerUidField)) + ) + .where(resource.isPublicField.eq(true)) + .fetch() + .asScala + .toList + .flatMap { record => + val entity = record.into(resource.table).into(pojoClass) + if (grantedIds.contains(idOf(entity))) None + else fromPublic(entity, record.into(USER).getEmail) + } granted.map(_._2) ++ public } diff --git a/file-service/src/test/scala/org/apache/texera/service/resource/ModelResourceSpec.scala b/file-service/src/test/scala/org/apache/texera/service/resource/ModelResourceSpec.scala index 218eb244707..01147f234e8 100644 --- a/file-service/src/test/scala/org/apache/texera/service/resource/ModelResourceSpec.scala +++ b/file-service/src/test/scala/org/apache/texera/service/resource/ModelResourceSpec.scala @@ -492,7 +492,7 @@ class ModelResourceSpec } } - "listModels" should "include public models owned by another user" in { + "listModels" should "omit public models owned by another user" in { val othersPublic = modelResource.createModel( ModelResource.CreateModelRequest( modelName = "others-public-model", @@ -505,10 +505,14 @@ class ModelResourceSpec sessionUser2 ) - val listed = modelResource.listModels(sessionUser) - val entry = listed.find(_.model.getMid == othersPublic.model.getMid) - entry should not be empty - entry.get.isOwner shouldBe false - entry.get.accessPrivilege shouldEqual PrivilegeEnum.READ + // /model/list backs the "Your Work" page, so it lists only what the caller was granted. + // Public models are discovered through the hub and fetched with getPublicModel. + modelResource + .listModels(sessionUser) + .find(_.model.getMid == othersPublic.model.getMid) shouldBe empty + modelResource + .getPublicModel(othersPublic.model.getMid) + .model + .getMid shouldEqual othersPublic.model.getMid } } diff --git a/frontend/proxy.config.json b/frontend/proxy.config.json index bc33371071e..7801e0c256f 100755 --- a/frontend/proxy.config.json +++ b/frontend/proxy.config.json @@ -35,6 +35,11 @@ "secure": false, "changeOrigin": true }, + "/api/model/**": { + "target": "http://localhost:9092", + "secure": false, + "changeOrigin": true + }, "/api/access/dataset/**": { "target": "http://localhost:9092", "secure": false, diff --git a/frontend/src/app/app-routing.constant.ts b/frontend/src/app/app-routing.constant.ts index 53cc71d79cc..3882984dd9b 100644 --- a/frontend/src/app/app-routing.constant.ts +++ b/frontend/src/app/app-routing.constant.ts @@ -35,6 +35,7 @@ export const USER_WORKSPACE = `${USER}/workflow`; export const USER_WORKFLOW = `${USER}/workflow`; export const USER_DATASET = `${USER}/dataset`; export const USER_DATASET_CREATE = `${USER_DATASET}/create`; +export const USER_MODEL = `${USER}/model`; export const USER_COMPUTING_UNIT = `${USER}/compute`; export const USER_PYTHON_VENV = `${USER}/python-venv`; export const USER_QUOTA = `${USER}/quota`; diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index e255fe16710..6acbccebf1e 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -39,6 +39,7 @@ import { FeedbackComponent } from "./dashboard/component/user/feedback/feedback. import { AdminGmailComponent } from "./dashboard/component/admin/gmail/admin-gmail.component"; import { DatasetDetailComponent } from "./dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component"; import { UserDatasetComponent } from "./dashboard/component/user/user-dataset/user-dataset.component"; +import { UserModelComponent } from "./dashboard/component/user/user-model/user-model.component"; import { HubWorkflowDetailComponent } from "./hub/component/workflow/detail/hub-workflow-detail.component"; import { LandingPageComponent } from "./hub/component/landing-page/landing-page.component"; import { USER_WORKFLOW } from "./app-routing.constant"; @@ -135,6 +136,10 @@ routes.push({ path: "dataset/create", component: DatasetDetailComponent, }, + { + path: "model", + component: UserModelComponent, + }, { path: "compute", component: UserComputingUnitComponent, diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index aae87fe4a93..f12d8c44ad1 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -22,6 +22,8 @@ import { GuiConfigService } from "./common/service/gui-config.service"; import { DeploymentVersionService } from "./common/service/deployment-version/deployment-version.service"; import { Version } from "../environments/version"; import { UntilDestroy } from "@ngneat/until-destroy"; +import { NzIconService } from "ng-zorro-antd/icon"; +import { MODEL_ICON, MODEL_ICON_SVG } from "./common/icon/model-icon"; @UntilDestroy() @Component({ @@ -44,8 +46,12 @@ export class AppComponent { constructor( private configService: GuiConfigService, - private deploymentVersionService: DeploymentVersionService + private deploymentVersionService: DeploymentVersionService, + iconService: NzIconService ) { + // ng-zorro has no icon that reads as an ML model, so this one is registered from Material Symbols. + iconService.addIconLiteral(MODEL_ICON, MODEL_ICON_SVG); + // determine whether configuration was successfully loaded by APP_INITIALIZER try { // accessing env will throw if not loaded diff --git a/frontend/src/app/common/icon/model-icon.ts b/frontend/src/app/common/icon/model-icon.ts new file mode 100644 index 00000000000..0f0b8417595 --- /dev/null +++ b/frontend/src/app/common/icon/model-icon.ts @@ -0,0 +1,11 @@ +/* + * Material Symbols "hub", from Google's material-design-icons. + * Apache License 2.0 — see the LICENSE file at the repository root for attribution. + * https://github.com/google/material-design-icons + */ + +/** Namespaced name to pass as `nzType`, once registered with NzIconService. */ +export const MODEL_ICON = "texera:model"; + +export const MODEL_ICON_SVG = + ''; diff --git a/frontend/src/app/common/type/gui-config.ts b/frontend/src/app/common/type/gui-config.ts index d9750d7d3b1..cf36f046eed 100644 --- a/frontend/src/app/common/type/gui-config.ts +++ b/frontend/src/app/common/type/gui-config.ts @@ -57,6 +57,7 @@ export interface SidebarTabs { workflows_enabled: boolean; compute_enabled: boolean; datasets_enabled: boolean; + models_enabled: boolean; quota_enabled: boolean; forum_enabled: boolean; about_enabled: boolean; diff --git a/frontend/src/app/common/type/model.ts b/frontend/src/app/common/type/model.ts new file mode 100644 index 00000000000..05e979249d9 --- /dev/null +++ b/frontend/src/app/common/type/model.ts @@ -0,0 +1,32 @@ +/** + * 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. + */ + +export interface Model { + mid: number | undefined; + ownerUid: number | undefined; + name: string; + repositoryName: string | undefined; + isPublic: boolean; + isDownloadable: boolean; + description: string; + creationTime: number | undefined; + coverImage: string | undefined; + framework: string | undefined; + format: string | undefined; +} diff --git a/frontend/src/app/dashboard/component/admin/settings/admin-settings.component.html b/frontend/src/app/dashboard/component/admin/settings/admin-settings.component.html index fd2fea7f753..2bfc1daaf00 100644 --- a/frontend/src/app/dashboard/component/admin/settings/admin-settings.component.html +++ b/frontend/src/app/dashboard/component/admin/settings/admin-settings.component.html @@ -190,6 +190,15 @@

General Settings

+ +