diff --git a/bootstrap/sql/migrations/native/2.0.0/mysql/schemaChanges.sql b/bootstrap/sql/migrations/native/2.0.0/mysql/schemaChanges.sql index 85c8362622b9..814df2c138bb 100644 --- a/bootstrap/sql/migrations/native/2.0.0/mysql/schemaChanges.sql +++ b/bootstrap/sql/migrations/native/2.0.0/mysql/schemaChanges.sql @@ -16,6 +16,7 @@ CREATE TABLE IF NOT EXISTS task_entity ( deleted tinyint(1) GENERATED ALWAYS AS (json_extract(`json`,_utf8mb4'$.deleted')) STORED, aboutFqnHash varchar(256) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.aboutFqnHash'))) STORED, createdById varchar(36) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.createdById'))) STORED, + approvedById varchar(36) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.approvedById'))) STORED, PRIMARY KEY (id), UNIQUE KEY uk_fqn_hash (fqnHash), KEY idx_task_id (taskId), @@ -30,9 +31,50 @@ CREATE TABLE IF NOT EXISTS task_entity ( KEY idx_about_fqn_hash (aboutFqnHash), KEY idx_status_about (status, aboutFqnHash), KEY idx_created_by_id (createdById), - KEY idx_created_by_category (createdById, category) + KEY idx_created_by_category (createdById, category), + KEY idx_approved_by_id (approvedById) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +-- For 2.0.0 environments that ran the CREATE TABLE above before the +-- approvedById generated column was added inline, attach it now. CREATE TABLE +-- IF NOT EXISTS is a no-op on those environments so the column would never +-- appear otherwise. MySQL doesn't reliably support `ADD COLUMN IF NOT EXISTS` +-- across 8.0 versions and has no `ADD KEY IF NOT EXISTS`, so guard both via +-- information_schema. +SET @ddl = ( + SELECT IF( + EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'task_entity' + AND column_name = 'approvedById' + ), + 'SELECT 1', + 'ALTER TABLE task_entity ADD COLUMN approvedById varchar(36) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4''$.approvedById''))) STORED' + ) +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @ddl = ( + SELECT IF( + EXISTS ( + SELECT 1 + FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = 'task_entity' + AND index_name = 'idx_approved_by_id' + ), + 'SELECT 1', + 'ALTER TABLE task_entity ADD KEY idx_approved_by_id (approvedById)' + ) +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + CREATE TABLE IF NOT EXISTS new_task_sequence ( id bigint NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; diff --git a/bootstrap/sql/migrations/native/2.0.0/postgres/schemaChanges.sql b/bootstrap/sql/migrations/native/2.0.0/postgres/schemaChanges.sql index 5b8b6a953f97..f544b9511cea 100644 --- a/bootstrap/sql/migrations/native/2.0.0/postgres/schemaChanges.sql +++ b/bootstrap/sql/migrations/native/2.0.0/postgres/schemaChanges.sql @@ -16,6 +16,7 @@ CREATE TABLE IF NOT EXISTS task_entity ( deleted boolean GENERATED ALWAYS AS (((json ->> 'deleted'::text))::boolean) STORED, aboutfqnhash character varying(256) GENERATED ALWAYS AS ((json ->> 'aboutFqnHash'::text)) STORED, createdbyid character varying(36) GENERATED ALWAYS AS ((json ->> 'createdById'::text)) STORED, + approvedbyid character varying(36) GENERATED ALWAYS AS ((json ->> 'approvedById'::text)) STORED, PRIMARY KEY (id), CONSTRAINT uk_task_fqn_hash UNIQUE (fqnhash) ); @@ -34,6 +35,19 @@ CREATE INDEX IF NOT EXISTS idx_task_status_about ON task_entity (status, aboutfq CREATE INDEX IF NOT EXISTS idx_task_created_by_id ON task_entity (createdbyid); CREATE INDEX IF NOT EXISTS idx_task_created_by_category ON task_entity (createdbyid, category); +-- For 2.0.0 environments that ran the CREATE TABLE above before the +-- approvedbyid generated column was added inline, attach it now. CREATE TABLE +-- IF NOT EXISTS is a no-op on those environments so the column would never +-- appear otherwise. Postgres supports `ADD COLUMN IF NOT EXISTS` natively. +-- The ALTER must run before idx_task_approved_by_id is created — otherwise +-- existing-2.0.0 deployments would fail the CREATE INDEX with "column does +-- not exist" before the ADD COLUMN ever runs. +ALTER TABLE task_entity + ADD COLUMN IF NOT EXISTS approvedbyid character varying(36) + GENERATED ALWAYS AS ((json ->> 'approvedById'::text)) STORED; + +CREATE INDEX IF NOT EXISTS idx_task_approved_by_id ON task_entity (approvedbyid); + CREATE TABLE IF NOT EXISTS new_task_sequence ( id bigint NOT NULL DEFAULT 0 ); diff --git a/openmetadata-integration-tests/pom.xml b/openmetadata-integration-tests/pom.xml index 5465bebeedc4..cb0662fc3feb 100644 --- a/openmetadata-integration-tests/pom.xml +++ b/openmetadata-integration-tests/pom.xml @@ -241,6 +241,25 @@ 2.3.0 test + + + org.apache.pdfbox + pdfbox + 2.0.31 + test + + + org.apache.poi + poi + 5.4.1 + test + + + org.apache.poi + poi-ooxml + 5.4.1 + test + diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataAccessRequestIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataAccessRequestIT.java index 140b8a38321c..903bfce76d14 100644 --- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataAccessRequestIT.java +++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataAccessRequestIT.java @@ -29,6 +29,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; +import org.openmetadata.it.bootstrap.SharedEntities; import org.openmetadata.it.factories.DatabaseSchemaTestFactory; import org.openmetadata.it.factories.DatabaseServiceTestFactory; import org.openmetadata.it.factories.TableTestFactory; @@ -49,6 +50,7 @@ import org.openmetadata.schema.type.TaskEntityType; import org.openmetadata.schema.type.TaskPriority; import org.openmetadata.schema.type.TaskResolutionType; +import org.openmetadata.sdk.exceptions.ApiException; import org.openmetadata.sdk.exceptions.InvalidRequestException; /** @@ -60,13 +62,15 @@ *
  • Seed: DataAccessRequest form schema and DataAccessRequestTaskWorkflow are loaded on boot. *
  • Create: POST /tasks with category=DataAccess, type=DataAccessRequest and an * accessType+reason payload succeeds and lands the task at the "review" stage. - *
  • Approve: /resolve transitions the task to status=InProgress, stage="approved", - * and surfaces a "revoke" available transition (matches the IncidentResolution pattern). - *
  • Revoke: /resolve from the approved stage closes the task with status=Revoked and - * resolution.type=Revoked. + *
  • Approve: /resolve transitions the task to status=Approved, stage="approved", + * captures approvedBy/approvedAt, and surfaces "markAsGranted" + "revoke" transitions. + *
  • Grant: /resolve with markAsGranted moves the task to status=Granted (active access). + *
  • Revoke: /resolve from either Approved or Granted closes the task with status=Revoked. *
  • Reject: alternative terminal path lands at status=Rejected. *
  • Validation: missing required fields (accessType/reason) are rejected by the form * schema validator. + *
  • Policy: non-admin users can create DARs via the DataConsumerPolicy Create-task rule. + *
  • Filters: /v1/tasks/dataAccessRequests honors status/accessType/requestedBy/sortOrder. * */ @Execution(ExecutionMode.CONCURRENT) @@ -133,12 +137,13 @@ void darWorkflowDefinitionIsSeeded() { List nodeNames = workflow.getNodes().stream().map(n -> n.getName()).toList(); assertTrue(nodeNames.contains("TaskReview")); assertTrue(nodeNames.contains("ApprovedAccess")); + assertTrue(nodeNames.contains("GrantedAccess")); assertTrue(nodeNames.contains("RejectedEnd")); assertTrue(nodeNames.contains("RevokedEnd")); } @Test - void createApproveAndRevokeLifecycle(TestNamespace ns) { + void createApproveGrantRevokeLifecycle(TestNamespace ns) { String tableFqn = createTargetTable(ns); Task created = @@ -167,8 +172,8 @@ void createApproveAndRevokeLifecycle(TestNamespace ns) { Task reviewed = reviewTaskRef.get(); - // Approve → moves to ApprovedAccess userTask. Status stays non-terminal so the workflow - // can continue to a Revoke transition (matches IncidentResolution pattern). + // Approve → status=Approved (awaiting grant). approvedBy/approvedAt captured. + // Available transitions: markAsGranted (provision) and revoke (back out). Task approved = SdkClients.adminClient() .tasks() @@ -176,26 +181,87 @@ void createApproveAndRevokeLifecycle(TestNamespace ns) { reviewed.getId().toString(), new ResolveTask().withTransitionId("approve").withComment("approved")); - assertEquals(TaskEntityStatus.InProgress, approved.getStatus()); + assertEquals(TaskEntityStatus.Approved, approved.getStatus()); assertEquals("approved", approved.getWorkflowStageId()); + assertNotNull(approved.getApprovedBy(), "approvedBy must be captured on approve transition"); + assertNotNull(approved.getApprovedById()); + assertNotNull(approved.getApprovedAt()); List approvedTransitions = approved.getAvailableTransitions().stream().map(TaskAvailableTransition::getId).toList(); - assertEquals(List.of("revoke"), approvedTransitions); + assertTrue(approvedTransitions.contains("markAsGranted")); + assertTrue(approvedTransitions.contains("revoke")); - // Revoke → terminal Revoked status with resolution. - Task revoked = + // Mark as granted → status=Granted (active access). + Task granted = SdkClients.adminClient() .tasks() .resolve( approved.getId().toString(), - new ResolveTask().withTransitionId("revoke").withComment("revoking")); + new ResolveTask().withTransitionId("markAsGranted").withComment("provisioned")); + + assertEquals(TaskEntityStatus.Granted, granted.getStatus()); + assertEquals("granted", granted.getWorkflowStageId()); + // approvedBy must persist through the grant transition. + assertEquals(approved.getApprovedById(), granted.getApprovedById()); + List grantedTransitions = + granted.getAvailableTransitions().stream().map(TaskAvailableTransition::getId).toList(); + assertEquals(List.of("revoke"), grantedTransitions); + + // Revoke from Granted → terminal Revoked status with resolution. + // Wrap the call in a short retry: the Task entity is already updated for the new + // GrantedAccess stage (asserted above), but the Flowable engine's runtime-task wait state + // occasionally hasn't settled the instant `markAsGranted` returns in CI. The next + // `resolveTask` then sees an active task without a matching pending transition and bubbles + // up `Workflow resolution failed`. A handful of poll attempts is enough to absorb the race. + AtomicReference revokedRef = new AtomicReference<>(); + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofMillis(250)) + .ignoreException(ApiException.class) + .untilAsserted( + () -> { + Task r = + SdkClients.adminClient() + .tasks() + .resolve( + granted.getId().toString(), + new ResolveTask().withTransitionId("revoke").withComment("revoking")); + assertEquals(TaskEntityStatus.Revoked, r.getStatus()); + revokedRef.set(r); + }); + Task revoked = revokedRef.get(); - assertEquals(TaskEntityStatus.Revoked, revoked.getStatus()); assertNotNull(revoked.getResolution()); assertEquals(TaskResolutionType.Revoked, revoked.getResolution().getType()); assertTrue(revoked.getAvailableTransitions().isEmpty()); } + @Test + void approvedCanBeRevokedWithoutGranting(TestNamespace ns) { + String tableFqn = createTargetTable(ns); + Task created = + SdkClients.adminClient().tasks().create(buildDarRequest(ns, tableFqn, "FullAccess")); + + Task approved = + SdkClients.adminClient() + .tasks() + .resolve( + created.getId().toString(), + new ResolveTask().withTransitionId("approve").withComment("approved")); + assertEquals(TaskEntityStatus.Approved, approved.getStatus()); + + // Revoke directly from the Approved stage (admin backs out before granting). + Task revoked = + SdkClients.adminClient() + .tasks() + .resolve( + approved.getId().toString(), + new ResolveTask().withTransitionId("revoke").withComment("backing out")); + + assertEquals(TaskEntityStatus.Revoked, revoked.getStatus()); + assertEquals(TaskResolutionType.Revoked, revoked.getResolution().getType()); + } + @Test void rejectLandsAtTerminalRejectedStatus(TestNamespace ns) { String tableFqn = createTargetTable(ns); @@ -256,4 +322,283 @@ void missingAccessTypeIsRejectedByFormSchema(TestNamespace ns) { assertThrows( InvalidRequestException.class, () -> SdkClients.adminClient().tasks().create(invalid)); } + + @Test + void nonAdminUserCanCreateDar(TestNamespace ns) { + // DataConsumerPolicy grants Create on resource=task to every authenticated user, so a + // non-admin user can file a DAR without an explicit role. Verifies the policy fix for the + // "Principal: ... operations [Create] not allowed" failure when adam.matthews2-style users + // tried to request access. + String tableFqn = createTargetTable(ns); + Task created = + SdkClients.user1Client().tasks().create(buildDarRequest(ns, tableFqn, "FullAccess")); + + assertNotNull(created.getId()); + assertEquals(TaskCategory.DataAccess, created.getCategory()); + assertEquals(TaskEntityType.DataAccessRequest, created.getType()); + } + + @Test + void darListEndpointFiltersByAccessTypeAndStatusAndSorts(TestNamespace ns) throws Exception { + String tableFqn = createTargetTable(ns); + + Task openFull = + SdkClients.adminClient().tasks().create(buildDarRequest(ns, tableFqn, "FullAccess")); + Task openColumn = + SdkClients.adminClient().tasks().create(buildDarRequest(ns, tableFqn, "ColumnLevel")); + Task approvedFull = + SdkClients.adminClient().tasks().create(buildDarRequest(ns, tableFqn, "FullAccess")); + SdkClients.adminClient() + .tasks() + .resolve( + approvedFull.getId().toString(), + new ResolveTask().withTransitionId("approve").withComment("approved")); + + // Filter by dataset → all three DARs come back (newest first by default sort DESC on + // createdAt). + var byDataset = + SdkClients.adminClient() + .tasks() + .listDataAccessRequests(Map.of("dataset", tableFqn, "limit", "50")); + List idsByDataset = + byDataset.getData().stream().map(t -> t.getId().toString()).toList(); + assertTrue(idsByDataset.contains(openFull.getId().toString())); + assertTrue(idsByDataset.contains(openColumn.getId().toString())); + assertTrue(idsByDataset.contains(approvedFull.getId().toString())); + + // Filter by accessType=ColumnLevel → only the ColumnLevel DAR comes back. + var byColumnAccess = + SdkClients.adminClient() + .tasks() + .listDataAccessRequests( + Map.of("dataset", tableFqn, "accessType", "ColumnLevel", "limit", "50")); + List columnIds = + byColumnAccess.getData().stream().map(t -> t.getId().toString()).toList(); + assertTrue(columnIds.contains(openColumn.getId().toString())); + assertFalse(columnIds.contains(openFull.getId().toString())); + assertFalse(columnIds.contains(approvedFull.getId().toString())); + + // Filter by status=Approved → only the approved DAR comes back. + var byApproved = + SdkClients.adminClient() + .tasks() + .listDataAccessRequests( + Map.of("dataset", tableFqn, "status", "Approved", "limit", "50")); + List approvedIds = + byApproved.getData().stream().map(t -> t.getId().toString()).toList(); + assertEquals(List.of(approvedFull.getId().toString()), approvedIds); + + // sortOrder=asc → oldest first; reverse of default DESC. Both lists span the same scope. + var ascending = + SdkClients.adminClient() + .tasks() + .listDataAccessRequests(Map.of("dataset", tableFqn, "sortOrder", "asc", "limit", "50")); + var descending = + SdkClients.adminClient() + .tasks() + .listDataAccessRequests( + Map.of("dataset", tableFqn, "sortOrder", "desc", "limit", "50")); + List ascIds = ascending.getData().stream().map(t -> t.getId().toString()).toList(); + List descIds = descending.getData().stream().map(t -> t.getId().toString()).toList(); + assertEquals(ascIds.size(), descIds.size()); + // The first id of the ascending list is the last id of the descending list and vice versa. + assertEquals(ascIds.get(0), descIds.get(descIds.size() - 1)); + assertEquals(ascIds.get(ascIds.size() - 1), descIds.get(0)); + } + + @Test + void darListEndpointFiltersByApprover(TestNamespace ns) throws Exception { + String tableFqn = createTargetTable(ns); + Task created = + SdkClients.adminClient().tasks().create(buildDarRequest(ns, tableFqn, "FullAccess")); + Task approved = + SdkClients.adminClient() + .tasks() + .resolve( + created.getId().toString(), + new ResolveTask().withTransitionId("approve").withComment("approved")); + + String approverId = approved.getApprovedById(); + assertNotNull(approverId, "approvedById must be captured on approve"); + + var byApprover = + SdkClients.adminClient() + .tasks() + .listDataAccessRequests( + Map.of("dataset", tableFqn, "approverId", approverId, "limit", "50")); + List ids = byApprover.getData().stream().map(t -> t.getId().toString()).toList(); + assertTrue(ids.contains(approved.getId().toString())); + // A DAR that was never approved by the same user must not appear. + Task openDar = + SdkClients.adminClient().tasks().create(buildDarRequest(ns, tableFqn, "FullAccess")); + var byApproverAgain = + SdkClients.adminClient() + .tasks() + .listDataAccessRequests( + Map.of("dataset", tableFqn, "approverId", approverId, "limit", "50")); + List idsAgain = + byApproverAgain.getData().stream().map(t -> t.getId().toString()).toList(); + assertFalse(idsAgain.contains(openDar.getId().toString())); + } + + @Test + void darListEndpointExcludesNonDarTaskTypes(TestNamespace ns) throws Exception { + // Verifies that /v1/tasks/dataAccessRequests pre-scopes to category=DataAccess + + // type=DataAccessRequest so non-DAR tasks (e.g. a description-update task) never appear. + String tableFqn = createTargetTable(ns); + + Task dar = SdkClients.adminClient().tasks().create(buildDarRequest(ns, tableFqn, "FullAccess")); + + // Create a non-DAR task about the same entity. + CreateTask nonDar = + new CreateTask() + .withName(ns.prefix("non-dar-task")) + .withCategory(TaskCategory.MetadataUpdate) + .withType(TaskEntityType.DescriptionUpdate) + .withAbout(tableEntityLink(tableFqn)) + .withPayload(Map.of("newDescription", "test")); + Task descTask = SdkClients.adminClient().tasks().create(nonDar); + + var listed = + SdkClients.adminClient() + .tasks() + .listDataAccessRequests(Map.of("dataset", tableFqn, "limit", "50")); + List ids = listed.getData().stream().map(t -> t.getId().toString()).toList(); + assertTrue(ids.contains(dar.getId().toString())); + assertFalse(ids.contains(descTask.getId().toString())); + } + + @Test + void darListEndpointSearchByDarSearchCondition(TestNamespace ns) throws Exception { + // q matches case-insensitively against name/displayName/payload.reason/about.* — verify the + // payload.reason path specifically, since that's what users typically search for. + String tableFqn = createTargetTable(ns); + + CreateTask quarterly = + new CreateTask() + .withName(ns.prefix("dar-quarterly")) + .withCategory(TaskCategory.DataAccess) + .withType(TaskEntityType.DataAccessRequest) + .withAbout(tableEntityLink(tableFqn)) + .withPayload( + Map.of( + "accessType", "FullAccess", + "reason", "Quarterly compliance review", + "duration", "P14D")); + Task qDar = SdkClients.adminClient().tasks().create(quarterly); + Task otherDar = + SdkClients.adminClient().tasks().create(buildDarRequest(ns, tableFqn, "FullAccess")); + + var matches = + SdkClients.adminClient() + .tasks() + .listDataAccessRequests(Map.of("dataset", tableFqn, "q", "quarterly", "limit", "50")); + List ids = matches.getData().stream().map(t -> t.getId().toString()).toList(); + assertTrue(ids.contains(qDar.getId().toString())); + assertFalse(ids.contains(otherDar.getId().toString())); + } + + @Test + void darListEndpointMultiSelectStatus(TestNamespace ns) throws Exception { + // status=Approved,Granted exercises the comma-separated SQL IN(...) path. + String tableFqn = createTargetTable(ns); + + Task openDar = + SdkClients.adminClient().tasks().create(buildDarRequest(ns, tableFqn, "FullAccess")); + + Task approvedDar = + SdkClients.adminClient().tasks().create(buildDarRequest(ns, tableFqn, "FullAccess")); + SdkClients.adminClient() + .tasks() + .resolve( + approvedDar.getId().toString(), + new ResolveTask().withTransitionId("approve").withComment("approved")); + + Task grantedDar = + SdkClients.adminClient().tasks().create(buildDarRequest(ns, tableFqn, "FullAccess")); + SdkClients.adminClient() + .tasks() + .resolve( + grantedDar.getId().toString(), + new ResolveTask().withTransitionId("approve").withComment("approved")); + SdkClients.adminClient() + .tasks() + .resolve( + grantedDar.getId().toString(), + new ResolveTask().withTransitionId("markAsGranted").withComment("granted")); + + var matches = + SdkClients.adminClient() + .tasks() + .listDataAccessRequests( + Map.of("dataset", tableFqn, "status", "Approved,Granted", "limit", "50")); + List ids = matches.getData().stream().map(t -> t.getId().toString()).toList(); + assertTrue(ids.contains(approvedDar.getId().toString())); + assertTrue(ids.contains(grantedDar.getId().toString())); + assertFalse(ids.contains(openDar.getId().toString())); + } + + @Test + void darListEndpointMultiSelectAccessType(TestNamespace ns) throws Exception { + // accessType=FullAccess,Masked exercises the JSON_EXTRACT IN(...) path. + String tableFqn = createTargetTable(ns); + + Task fullDar = + SdkClients.adminClient().tasks().create(buildDarRequest(ns, tableFqn, "FullAccess")); + Task maskedDar = + SdkClients.adminClient().tasks().create(buildDarRequest(ns, tableFqn, "Masked")); + Task columnLevelDar = + SdkClients.adminClient().tasks().create(buildDarRequest(ns, tableFqn, "ColumnLevel")); + + var matches = + SdkClients.adminClient() + .tasks() + .listDataAccessRequests( + Map.of( + "dataset", tableFqn, + "accessType", "FullAccess,Masked", + "limit", "50")); + List ids = matches.getData().stream().map(t -> t.getId().toString()).toList(); + assertTrue(ids.contains(fullDar.getId().toString())); + assertTrue(ids.contains(maskedDar.getId().toString())); + assertFalse(ids.contains(columnLevelDar.getId().toString())); + } + + @Test + void darListEndpointAssigneeFilter(TestNamespace ns) throws Exception { + // assignee= walks the entity_relationship + nameHash join already used by + // /v1/tasks. Multi-value (comma-separated) hits the IN-list branch. + String tableFqn = createTargetTable(ns); + String assignee1 = SharedEntities.get().USER1.getFullyQualifiedName(); + String assignee2 = SharedEntities.get().USER2.getFullyQualifiedName(); + + Task dar1 = + SdkClients.adminClient() + .tasks() + .create(buildDarRequest(ns, tableFqn, "FullAccess").withAssignees(List.of(assignee1))); + Task dar2 = + SdkClients.adminClient() + .tasks() + .create(buildDarRequest(ns, tableFqn, "FullAccess").withAssignees(List.of(assignee2))); + + var singleAssignee = + SdkClients.adminClient() + .tasks() + .listDataAccessRequests( + Map.of("dataset", tableFqn, "assignee", assignee1, "limit", "50")); + List singleIds = + singleAssignee.getData().stream().map(t -> t.getId().toString()).toList(); + assertTrue(singleIds.contains(dar1.getId().toString())); + assertFalse(singleIds.contains(dar2.getId().toString())); + + var bothAssignees = + SdkClients.adminClient() + .tasks() + .listDataAccessRequests( + Map.of( + "dataset", tableFqn, "assignee", assignee1 + "," + assignee2, "limit", "50")); + List bothIds = bothAssignees.getData().stream().map(t -> t.getId().toString()).toList(); + assertTrue(bothIds.contains(dar1.getId().toString())); + assertTrue(bothIds.contains(dar2.getId().toString())); + } } diff --git a/openmetadata-sdk/src/main/java/org/openmetadata/sdk/services/tasks/TaskService.java b/openmetadata-sdk/src/main/java/org/openmetadata/sdk/services/tasks/TaskService.java index 3d080eef4803..a25524f5f937 100644 --- a/openmetadata-sdk/src/main/java/org/openmetadata/sdk/services/tasks/TaskService.java +++ b/openmetadata-sdk/src/main/java/org/openmetadata/sdk/services/tasks/TaskService.java @@ -215,6 +215,31 @@ public ListResponse listVisible( return deserializeListResponse(responseStr); } + /** + * List Data Access Requests with DAR-specific filters and offset-based pagination. + * Pre-applies category=DataAccess and type=DataAccessRequest server-side. + * + * @param filters Optional filters (dataset, service, status, statusGroup, requestedBy, + * requestedById, approver, approverId, accessType, domain, sortOrder, limit, offset, + * include, fields). + */ + public ListResponse listDataAccessRequests(Map filters) + throws OpenMetadataException { + String path = basePath + "/dataAccessRequests"; + RequestOptions.Builder optionsBuilder = RequestOptions.builder(); + if (filters != null) { + filters.forEach( + (k, v) -> { + if (v != null) { + optionsBuilder.queryParam(k, v); + } + }); + } + String responseStr = + httpClient.executeForString(HttpMethod.GET, path, null, optionsBuilder.build()); + return deserializeListResponse(responseStr); + } + // ==================== Comment Methods ==================== /** diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/governance/workflows/elements/nodes/userTask/CreateTask.java b/openmetadata-service/src/main/java/org/openmetadata/service/governance/workflows/elements/nodes/userTask/CreateTask.java index 9440f85e85c3..2ce775cfcee6 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/governance/workflows/elements/nodes/userTask/CreateTask.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/governance/workflows/elements/nodes/userTask/CreateTask.java @@ -627,7 +627,9 @@ static boolean isTerminalTaskStatus(TaskEntityStatus status) { return status != null && status != TaskEntityStatus.Open && status != TaskEntityStatus.InProgress - && status != TaskEntityStatus.Pending; + && status != TaskEntityStatus.Pending + && status != TaskEntityStatus.Approved + && status != TaskEntityStatus.Granted; } static boolean shouldSkipDeletedWorkflowManagedDraftTask( diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java index becf9ad70657..8c66ec043c56 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java @@ -4167,12 +4167,17 @@ class TaskCountSummary { private final int open; private final int completed; private final int inProgress; + private final int approved; + private final int granted; - public TaskCountSummary(int total, int open, int completed, int inProgress) { + public TaskCountSummary( + int total, int open, int completed, int inProgress, int approved, int granted) { this.total = total; this.open = open; this.completed = completed; this.inProgress = inProgress; + this.approved = approved; + this.granted = granted; } public int getTotal() { @@ -4190,6 +4195,14 @@ public int getCompleted() { public int getInProgress() { return inProgress; } + + public int getApproved() { + return approved; + } + + public int getGranted() { + return granted; + } } class TaskCountSummaryMapper implements RowMapper { @@ -4199,7 +4212,9 @@ public TaskCountSummary map(ResultSet rs, StatementContext ctx) throws SQLExcept rs.getInt("total"), rs.getInt("openCount"), rs.getInt("completedCount"), - rs.getInt("inProgressCount")); + rs.getInt("inProgressCount"), + rs.getInt("approvedCount"), + rs.getInt("grantedCount")); } } @@ -4336,14 +4351,36 @@ List listIdAndFqnByCreatorAndCategory( @RegisterRowMapper(TaskCountSummaryMapper.class) @SqlQuery( + // 'Approved' double-counts in `completedCount` AND `approvedCount` because the + // same status means different things across task types: terminal for + // Glossary/DescriptionUpdate (legacy dashboards expect it under "completed") and + // non-terminal for Data Access Requests (the dedicated DAR list uses + // `approvedCount` / `grantedCount` and the `active` status group instead). + // See ListFilter.getTaskStatusCondition for the matching status-group semantics. "SELECT " + "COUNT(id) AS total, " - + "COALESCE(SUM(CASE WHEN status IN ('Open', 'InProgress') THEN 1 ELSE 0 END), 0) AS openCount, " - + "COALESCE(SUM(CASE WHEN status IN ('Approved', 'Rejected', 'Completed', 'Cancelled', 'Failed') THEN 1 ELSE 0 END), 0) AS completedCount, " - + "COALESCE(SUM(CASE WHEN status = 'InProgress' THEN 1 ELSE 0 END), 0) AS inProgressCount " + + "COALESCE(SUM(CASE WHEN status IN ('Open', 'InProgress', 'Pending') THEN 1 ELSE 0 END), 0) AS openCount, " + + "COALESCE(SUM(CASE WHEN status IN ('Approved', 'Rejected', 'Completed', 'Cancelled', 'Failed', 'Revoked') THEN 1 ELSE 0 END), 0) AS completedCount, " + + "COALESCE(SUM(CASE WHEN status = 'InProgress' THEN 1 ELSE 0 END), 0) AS inProgressCount, " + + "COALESCE(SUM(CASE WHEN status = 'Approved' THEN 1 ELSE 0 END), 0) AS approvedCount, " + + "COALESCE(SUM(CASE WHEN status = 'Granted' THEN 1 ELSE 0 END), 0) AS grantedCount " + "FROM task_entity ") TaskCountSummary getTaskCountSummary( @Define("condition") String condition, @BindMap Map params); + + @SqlQuery( + "SELECT json FROM task_entity " + + "ORDER BY createdAt , id " + + "LIMIT :limit OFFSET :offset") + List listTasksByCreatedAt( + @Define("cond") String cond, + @BindMap Map params, + @Define("sortOrder") String sortOrder, + @Bind("limit") int limit, + @Bind("offset") int offset); + + @SqlQuery("SELECT count(*) FROM task_entity ") + int listTasksByCreatedAtCount(@Define("cond") String cond, @BindMap Map params); } interface AnnouncementDAO extends EntityDAO { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ListFilter.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ListFilter.java index 1224a1dde596..b7e21d374e50 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ListFilter.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ListFilter.java @@ -81,6 +81,10 @@ public String getCondition(String tableName) { conditions.add(getTaskFormCategoryCondition(tableName)); conditions.add(getTaskTypeCondition(tableName)); conditions.add(getTaskPriorityCondition(tableName)); + conditions.add(getTaskApproverCondition()); + conditions.add(getTaskAboutServiceCondition()); + conditions.add(getTaskAccessTypeCondition()); + conditions.add(getDarSearchCondition()); conditions.add(getEntityStatusCondition(tableName)); conditions.add(getServerIdCondition(tableName)); conditions.add(getNameFilterCondition()); @@ -146,23 +150,31 @@ private String getAssignee() { } String assigneeFqn = queryParams.get("assignee"); - if (assigneeFqn == null) { + if (nullOrEmpty(assigneeFqn)) { return ""; } - String assigneeFqnHash = FullyQualifiedName.buildHash(assigneeFqn); - queryParams.put("assigneeFqnHashParam", assigneeFqnHash); + String hashCsv = + Arrays.stream(assigneeFqn.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(FullyQualifiedName::buildHash) + .collect(Collectors.joining(",")); + String inCondition = buildIndexedBindParams("assigneeFqnHash", hashCsv); return String.format( "(id IN (SELECT er.toId FROM entity_relationship er " + "INNER JOIN user_entity u ON er.fromId = u.id " + "WHERE er.fromEntity = 'user' " - + "AND u.nameHash = :assigneeFqnHashParam " + + "AND u.nameHash IN (%s) " + "AND er.relation = %d) " + "OR id IN (SELECT er.toId FROM entity_relationship er " + "INNER JOIN team_entity t ON er.fromId = t.id " + "WHERE er.fromEntity = 'team' " - + "AND t.nameHash = :assigneeFqnHashParam " + + "AND t.nameHash IN (%s) " + "AND er.relation = %d))", - Relationship.ASSIGNED_TO.ordinal(), Relationship.ASSIGNED_TO.ordinal()); + inCondition, + Relationship.ASSIGNED_TO.ordinal(), + inCondition, + Relationship.ASSIGNED_TO.ordinal()); } /** @@ -172,13 +184,10 @@ private String getAssignee() { */ private String getAboutEntityCondition() { String aboutEntityFqn = queryParams.get("aboutEntity"); - if (aboutEntityFqn == null) { + if (nullOrEmpty(aboutEntityFqn)) { return ""; } - String fqnHash = FullyQualifiedName.buildHash(aboutEntityFqn); - queryParams.put("aboutFqnHashParam", fqnHash); - queryParams.put("aboutFqnHashPrefixParam", fqnHash + ".%"); - return "(aboutFqnHash = :aboutFqnHashParam OR aboutFqnHash LIKE :aboutFqnHashPrefixParam)"; + return buildFqnPrefixOrCondition("about", aboutEntityFqn); } /** @@ -207,24 +216,29 @@ private String getMentionedUserCondition() { */ private String getCreatedByCondition() { String createdById = queryParams.get("createdById"); - if (createdById != null) { - queryParams.put("createdByIdParam", createdById); - return "createdById = :createdByIdParam"; + if (!nullOrEmpty(createdById)) { + String inCondition = buildIndexedBindParams("createdById", createdById); + return String.format("createdById IN (%s)", inCondition); } String createdBy = queryParams.get("createdBy"); - if (createdBy == null) { + if (nullOrEmpty(createdBy)) { return ""; } - String createdByFqnHash = FullyQualifiedName.buildHash(createdBy); - queryParams.put("createdByFqnHashParam", createdByFqnHash); + String hashCsv = + Arrays.stream(createdBy.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(FullyQualifiedName::buildHash) + .collect(Collectors.joining(",")); + String inCondition = buildIndexedBindParams("createdByFqnHash", hashCsv); return String.format( "(id IN (SELECT er.toId FROM entity_relationship er " + "INNER JOIN user_entity u ON er.fromId = u.id " + "WHERE er.fromEntity = 'user' " - + "AND u.nameHash = :createdByFqnHashParam " + + "AND u.nameHash IN (%s) " + "AND er.relation = %d))", - Relationship.CREATED.ordinal()); + inCondition, Relationship.CREATED.ordinal()); } private String getWorkflowDefinitionIdCondition() { @@ -977,20 +991,140 @@ private String getTaskStatusCondition(String tableName) { String column = tableName == null ? "status" : tableName + ".status"; if ("open".equalsIgnoreCase(statusGroup)) { return String.format("%s IN ('Open', 'InProgress', 'Pending')", column); + } else if ("active".equalsIgnoreCase(statusGroup)) { + return String.format( + "%s IN ('Open', 'InProgress', 'Pending', 'Approved', 'Granted')", column); } else if ("closed".equalsIgnoreCase(statusGroup)) { + // 'Approved' is intentionally a member of both 'active' and 'closed' because the + // same status maps to different lifecycle meanings depending on the task type: + // - Glossary/DescriptionUpdate/etc.: 'Approved' is the terminal state and must + // surface in the existing Closed tab. + // - DataAccessRequest: 'Approved' means "awaiting grant" — non-terminal — and + // callers reach those tasks via the 'active' group instead. + // Removing 'Approved' here would regress the Closed tab UX for the older workflows. + // A future refactor could make status group resolution task-type aware. return String.format( - "%s IN ('Approved', 'Rejected', 'Completed', 'Cancelled', 'Failed')", column); + "%s IN ('Approved', 'Rejected', 'Completed', 'Cancelled', 'Failed', 'Revoked')", + column); } } String taskStatus = queryParams.get("taskStatus"); - if (taskStatus == null) { + if (nullOrEmpty(taskStatus)) { return ""; } - String safeStatus = escapeApostrophe(taskStatus); - return tableName == null - ? String.format("status = '%s'", safeStatus) - : String.format("%s.status = '%s'", tableName, safeStatus); + String column = tableName == null ? "status" : tableName + ".status"; + String inCondition = buildIndexedBindParams("taskStatus", taskStatus); + return String.format("%s IN (%s)", column, inCondition); + } + + private String getTaskApproverCondition() { + String approvedById = queryParams.get("approverId"); + if (!nullOrEmpty(approvedById)) { + String inCondition = buildIndexedBindParams("approverId", approvedById); + return String.format("approvedById IN (%s)", inCondition); + } + + String approverFqn = queryParams.get("approver"); + if (nullOrEmpty(approverFqn)) { + return ""; + } + String hashCsv = + Arrays.stream(approverFqn.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(FullyQualifiedName::buildHash) + .collect(Collectors.joining(",")); + String inCondition = buildIndexedBindParams("approverFqnHash", hashCsv); + return String.format( + "(approvedById IN (SELECT u.id FROM user_entity u WHERE u.nameHash IN (%s)))", inCondition); + } + + private String getTaskAboutServiceCondition() { + String serviceFqn = queryParams.get("aboutService"); + if (nullOrEmpty(serviceFqn)) { + return ""; + } + return buildFqnPrefixOrCondition("aboutService", serviceFqn); + } + + private String getTaskAccessTypeCondition() { + String accessType = queryParams.get("accessType"); + if (nullOrEmpty(accessType)) { + return ""; + } + String inCondition = buildIndexedBindParams("accessType", accessType); + if (Boolean.TRUE.equals(DatasourceConfig.getInstance().isMySQL())) { + return String.format( + "JSON_UNQUOTE(JSON_EXTRACT(json, '$.payload.accessType')) IN (%s)", inCondition); + } + return String.format("json->'payload'->>'accessType' IN (%s)", inCondition); + } + + /** + * Free-text search across DAR-relevant fields. Used by the {@code q} query param on + * {@code /v1/tasks/dataAccessRequests}. Database-only — DARs are not indexed into Elasticsearch. + * Matches against task name, displayName, the DAR payload.reason, and the about-entity FQN / + * displayName. + */ + private String getDarSearchCondition() { + String search = queryParams.get("darSearch"); + if (nullOrEmpty(search)) { + return ""; + } + // escape() handles `'` and `_`, but leaves `%` alone (callers like + // getCategoryPrefixCondition want trailing `%` as a wildcard). For free-text search the + // anchor wildcards we add below are the only ones allowed; escape `%` inside the user + // input so callers can't probe rows via `q=%` or smuggle wildcards into the middle. + String escaped = "%" + escape(search.trim()).replace("%", "\\%") + "%"; + queryParams.put("darSearchParam", escaped); + if (Boolean.TRUE.equals(DatasourceConfig.getInstance().isMySQL())) { + return "(LOWER(name) LIKE LOWER(:darSearchParam) " + + "OR LOWER(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(json, '$.displayName')), '')) LIKE LOWER(:darSearchParam) " + + "OR LOWER(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(json, '$.payload.reason')), '')) LIKE LOWER(:darSearchParam) " + + "OR LOWER(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(json, '$.about.displayName')), '')) LIKE LOWER(:darSearchParam) " + + "OR LOWER(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(json, '$.about.fullyQualifiedName')), '')) LIKE LOWER(:darSearchParam))"; + } + return "(LOWER(name) LIKE LOWER(:darSearchParam) " + + "OR LOWER(COALESCE(json->>'displayName', '')) LIKE LOWER(:darSearchParam) " + + "OR LOWER(COALESCE(json->'payload'->>'reason', '')) LIKE LOWER(:darSearchParam) " + + "OR LOWER(COALESCE(json->'about'->>'displayName', '')) LIKE LOWER(:darSearchParam) " + + "OR LOWER(COALESCE(json->'about'->>'fullyQualifiedName', '')) LIKE LOWER(:darSearchParam))"; + } + + /** + * Shared helper for the task_entity multi-value FQN filters (aboutEntity, aboutService). + * Both filters target the same generated column, {@code task_entity.aboutFqnHash}: an + * "aboutEntity" filter matches the dataset's FQN-hash exactly or as a prefix, and an + * "aboutService" filter matches the parent service's FQN-hash as a prefix of any + * dataset's FQN-hash beneath it. Splits the comma-separated input, hashes each FQN, and + * produces an OR-joined fragment of {@code (aboutFqnHash = :hash OR aboutFqnHash LIKE + * :hash_prefix)} groups. + * + *

    This helper deliberately hard-codes {@code aboutFqnHash}; the {@code prefix} arg + * only namespaces the bound parameter keys. Don't reuse it for other columns — copy and + * adjust instead so the column choice stays explicit at the callsite. + */ + private String buildFqnPrefixOrCondition(String prefix, String commaSeparatedFqns) { + List tokens = + Arrays.stream(commaSeparatedFqns.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .toList(); + if (tokens.isEmpty()) { + return ""; + } + List clauses = new ArrayList<>(); + for (int i = 0; i < tokens.size(); i++) { + String hash = FullyQualifiedName.buildHash(tokens.get(i)); + String hashKey = prefix + "FqnHash_" + i; + String prefixKey = prefix + "FqnHashPrefix_" + i; + queryParams.put(hashKey, hash); + queryParams.put(prefixKey, hash + ".%"); + clauses.add( + String.format("(aboutFqnHash = :%s OR aboutFqnHash LIKE :%s)", hashKey, prefixKey)); + } + return clauses.size() == 1 ? clauses.get(0) : "(" + String.join(" OR ", clauses) + ")"; } private String getTaskTypeCondition(String tableName) { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TaskRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TaskRepository.java index fcf9a6ccad19..6126b6c478d1 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TaskRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TaskRepository.java @@ -156,6 +156,21 @@ public ResultList listBefore( return super.listBefore(uriInfo, fields, filter, limitParam, before); } + public ResultList listDataAccessRequests( + UriInfo uriInfo, Fields fields, ListFilter filter, int limit, int offset, String sortOrder) { + applyTaskDomainFilter(filter); + String direction = "ASC".equalsIgnoreCase(sortOrder) ? "ASC" : "DESC"; + CollectionDAO.TaskDAO taskDAO = (CollectionDAO.TaskDAO) dao; + int total = taskDAO.listTasksByCreatedAtCount(filter.getCondition(), filter.getQueryParams()); + List jsons = + taskDAO.listTasksByCreatedAt( + filter.getCondition(), filter.getQueryParams(), direction, limit, offset); + List entities = JsonUtils.readObjects(jsons, Task.class); + setFieldsInBulk(fields, entities); + entities.forEach(entity -> withHref(uriInfo, entity)); + return new ResultList<>(entities, offset, limit, total); + } + public void addDomainFilter(ListFilter filter, String domainFilter) { if (nullOrEmpty(domainFilter)) { return; @@ -1011,6 +1026,19 @@ private MetadataOperation getOperationForSuggestion(Task task) { * Internal method to update task resolution status. * Called by TaskWorkflowHandler after workflow processing. */ + public Task persistApprover(UUID taskId, EntityReference approver, String updatedBy) { + Task original = get(null, taskId, getFields("*")); + Task updated = JsonUtils.deepCopy(original, Task.class); + updated.setApprovedBy(approver); + updated.setApprovedById(approver.getId() != null ? approver.getId().toString() : null); + updated.setApprovedAt(System.currentTimeMillis()); + updated.setUpdatedBy(updatedBy); + updated.setUpdatedAt(System.currentTimeMillis()); + storeEntity(updated, true); + postUpdate(original, updated); + return updated; + } + public Task resolveTask(Task task, TaskResolution resolution, String updatedBy) { if (resolution == null) { throw new IllegalArgumentException("Resolution cannot be null"); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/tasks/TaskResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/tasks/TaskResource.java index 584727e4bd23..283485b97688 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/tasks/TaskResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/tasks/TaskResource.java @@ -46,9 +46,12 @@ import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.core.UriInfo; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.openmetadata.schema.api.tasks.BulkTaskOperation; import org.openmetadata.schema.api.tasks.CreateTask; @@ -61,6 +64,7 @@ import org.openmetadata.schema.type.BulkTaskOperationResult; import org.openmetadata.schema.type.BulkTaskOperationResultItem; import org.openmetadata.schema.type.BulkTaskOperationType; +import org.openmetadata.schema.type.DataAccessType; import org.openmetadata.schema.type.EntityHistory; import org.openmetadata.schema.type.EntityReference; import org.openmetadata.schema.type.Include; @@ -84,8 +88,11 @@ import org.openmetadata.service.resources.feeds.MessageParser.EntityLink; import org.openmetadata.service.security.AuthorizationException; import org.openmetadata.service.security.Authorizer; +import org.openmetadata.service.security.policyevaluator.OperationContext; +import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; import org.openmetadata.service.security.policyevaluator.SubjectContext; import org.openmetadata.service.tasks.TaskWorkflowLifecycleResolver; +import org.openmetadata.service.util.EntityUtil; import org.openmetadata.service.util.EntityUtil.Fields; @Slf4j @@ -155,6 +162,14 @@ public ResultList list( UUID createdById, @Parameter(description = "Filter by entity FQN the task is about") @QueryParam("aboutEntity") String aboutEntity, + @Parameter(description = "Filter by parent service FQN of the entity the task is about") + @QueryParam("aboutService") + String aboutService, + @Parameter(description = "Filter by approver FQN (user who approved the task)") + @QueryParam("approver") + String approver, + @Parameter(description = "Filter by approver user id") @QueryParam("approverId") + UUID approverId, @Parameter(description = "Filter by user FQN who was mentioned in task comments") @QueryParam("mentionedUser") String mentionedUser, @@ -200,6 +215,15 @@ public ResultList list( if (aboutEntity != null) { filter.addQueryParam("aboutEntity", aboutEntity); } + if (aboutService != null) { + filter.addQueryParam("aboutService", aboutService); + } + if (approver != null) { + filter.addQueryParam("approver", approver); + } + if (approverId != null) { + filter.addQueryParam("approverId", approverId.toString()); + } if (mentionedUser != null) { filter.addQueryParam("mentionedUser", mentionedUser); } @@ -261,11 +285,183 @@ public Response getTaskCount( .withOpen(countSummary.getOpen()) .withCompleted(countSummary.getCompleted()) .withInProgress(countSummary.getInProgress()) + .withApproved(countSummary.getApproved()) + .withGranted(countSummary.getGranted()) .withTotal(countSummary.getTotal()); return Response.ok(response).build(); } + @GET + @Path("/dataAccessRequests") + @Operation( + operationId = "listDataAccessRequests", + summary = "List data access requests", + description = + "Get a paginated list of Data Access Request tasks with DAR-specific filters. " + + "Pre-applies category=DataAccess and type=DataAccessRequest. " + + "Pagination is offset-based and results are sorted by createdAt (default DESC).", + responses = { + @ApiResponse( + responseCode = "200", + description = "List of Data Access Requests", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = TaskList.class))) + }) + public ResultList listDataAccessRequests( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "Fields to include in response", schema = @Schema(type = "string")) + @QueryParam("fields") + String fieldsParam, + @Parameter( + description = + "Filter by task status. Accepts a comma-separated list (e.g. 'Approved,Granted') which is matched as SQL IN(...). Allowed values match TaskEntityStatus.") + @QueryParam("status") + String status, + @Parameter( + description = + "Filter by status group. 'open' = Open/InProgress/Pending only (still awaiting review). " + + "'active' = Open/InProgress/Pending/Approved/Granted (full non-terminal DAR lifecycle — use this when looking for " + + "DARs that are still 'in progress', including awaiting-grant and granted-with-active-access). " + + "'closed' = Approved/Rejected/Completed/Cancelled/Failed/Revoked (mirrors the legacy closed bucket, which includes " + + "Approved for backward compatibility with non-DAR workflows where Approved is terminal).", + schema = + @Schema( + type = "string", + allowableValues = {"open", "active", "closed"})) + @QueryParam("statusGroup") + String statusGroup, + @Parameter( + description = + "Filter by dataset FQN (entity the DAR is about). Accepts a comma-separated list; matches each via FQN-hash prefix and OR's the results.") + @QueryParam("dataset") + String dataset, + @Parameter( + description = + "Filter by parent service FQN of the dataset. Accepts a comma-separated list (OR-joined).") + @QueryParam("service") + String service, + @Parameter( + description = "Filter by requester FQN. Accepts a comma-separated list (OR-joined).") + @QueryParam("requestedBy") + String requestedBy, + @Parameter( + description = + "Filter by requester user id. Accepts a comma-separated list of UUIDs matched via SQL IN(...).") + @QueryParam("requestedById") + String requestedById, + @Parameter( + description = "Filter by approver FQN. Accepts a comma-separated list (OR-joined).") + @QueryParam("approver") + String approver, + @Parameter( + description = + "Filter by approver user id. Accepts a comma-separated list of UUIDs matched via SQL IN(...).") + @QueryParam("approverId") + String approverId, + @Parameter( + description = + "Filter by access type. Accepts a comma-separated list (e.g. 'FullAccess,Masked') matched via SQL IN(...). Allowed values match DataAccessType.") + @QueryParam("accessType") + String accessType, + @Parameter( + description = + "Filter by assignee FQN (user or team). Accepts a comma-separated list (OR-joined).") + @QueryParam("assignee") + String assignee, + @Parameter(description = "Filter by assignee user/team id (single UUID).") + @QueryParam("assigneeId") + UUID assigneeId, + @Parameter(description = "Filter by domain FQN") @QueryParam("domain") String domain, + @Parameter( + description = + "Free-text search. Database-only (DARs are not indexed into Elasticsearch). " + + "Matches case-insensitive against task name, displayName, payload.reason, about.displayName, and about.fullyQualifiedName.") + @QueryParam("q") + String q, + @Parameter( + description = "Sort order on createdAt", + schema = + @Schema( + type = "string", + allowableValues = {"asc", "desc"})) + @QueryParam("sortOrder") + @DefaultValue("desc") + String sortOrder, + @Parameter(description = "Limit the number of results", schema = @Schema(type = "integer")) + @DefaultValue("10") + @QueryParam("limit") + @Min(0) + @Max(1000000) + int limitParam, + @Parameter(description = "Offset into the result set", schema = @Schema(type = "integer")) + @DefaultValue("0") + @QueryParam("offset") + @Min(0) + int offset, + @Parameter(description = "Include deleted tasks") + @QueryParam("include") + @DefaultValue("non-deleted") + Include include) { + ListFilter filter = new ListFilter(include); + filter.addQueryParam("category", TaskCategory.DataAccess.value()); + filter.addQueryParam("taskType", TaskEntityType.DataAccessRequest.value()); + + if (statusGroup != null) { + filter.addQueryParam("taskStatusGroup", statusGroup); + } else if (!nullOrEmpty(status)) { + validateCsvAgainstEnum("status", status, TaskEntityStatus.class); + filter.addQueryParam("taskStatus", status); + } + if (!nullOrEmpty(dataset)) { + filter.addQueryParam("aboutEntity", dataset); + } + if (!nullOrEmpty(service)) { + filter.addQueryParam("aboutService", service); + } + if (!nullOrEmpty(requestedBy)) { + filter.addQueryParam("createdBy", requestedBy); + } + if (!nullOrEmpty(requestedById)) { + filter.addQueryParam("createdById", requestedById); + } + if (!nullOrEmpty(approver)) { + filter.addQueryParam("approver", approver); + } + if (!nullOrEmpty(approverId)) { + filter.addQueryParam("approverId", approverId); + } + if (!nullOrEmpty(accessType)) { + validateCsvAgainstAccessType(accessType); + filter.addQueryParam("accessType", accessType); + } + if (!nullOrEmpty(assignee)) { + filter.addQueryParam("assignee", assignee); + } + if (assigneeId != null) { + filter.addQueryParam("assigneeId", assigneeId.toString()); + } + if (!nullOrEmpty(q)) { + filter.addQueryParam("darSearch", q); + } + repository.addDomainFilter(filter, domain); + + Fields fields = getFields(fieldsParam); + // Mirror the auth + domain-scoping that listInternal applies on the generic /v1/tasks + // endpoint. We don't reuse listInternal directly because this endpoint is offset-paginated + // and sorts by createdAt rather than the cursor-based (name, id) pagination listInternal uses. + OperationContext operationContext = new OperationContext(entityType, getViewOperations(fields)); + ResourceContextInterface resourceContext = filter.getResourceContext(entityType); + authorizer.authorize(securityContext, operationContext, resourceContext); + EntityUtil.addDomainQueryParam(securityContext, filter, entityType); + + return repository.listDataAccessRequests( + uriInfo, fields, filter, limitParam, offset, sortOrder); + } + @GET @Path("/assigned") @Operation( @@ -1197,6 +1393,36 @@ private void processBulkOperation( } } + /** + * Per-token validation for a comma-separated enum query param. Surfaces a 400 if any token + * isn't a recognized {@link Enum} value, so callers see a clear error instead of an opaque + * empty result set or a downstream SQL surprise. + */ + private > void validateCsvAgainstEnum( + String paramName, String csv, Class enumClass) { + Set allowed = + Arrays.stream(enumClass.getEnumConstants()).map(Enum::name).collect(Collectors.toSet()); + Arrays.stream(csv.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .forEach( + token -> { + if (!allowed.contains(token)) { + throw BadRequestException.of( + String.format( + "Invalid '%s' value '%s'. Allowed values: %s", paramName, token, allowed)); + } + }); + } + + /** + * Per-token validation for the {@code accessType} CSV. Reuses the schema-generated + * {@link DataAccessType} enum. + */ + private void validateCsvAgainstAccessType(String csv) { + validateCsvAgainstEnum("accessType", csv, DataAccessType.class); + } + private void validateTaskCanBeResolved(Task task) { TaskEntityStatus status = task.getStatus(); if (status == TaskEntityStatus.Open @@ -1205,6 +1431,17 @@ private void validateTaskCanBeResolved(Task task) { return; } + // Approved and Granted are non-terminal only for workflows that expose further + // transitions out of them (Data Access Request: Approved → markAsGranted/revoke, + // Granted → revoke). For workflows where Approved is terminal (Glossary, + // DescriptionUpdate, etc.), availableTransitions is empty and the task must stay + // closed — re-resolving it would re-run postUpdate hooks and clobber resolution. + if ((status == TaskEntityStatus.Approved || status == TaskEntityStatus.Granted) + && task.getAvailableTransitions() != null + && !task.getAvailableTransitions().isEmpty()) { + return; + } + throw BadRequestException.of( String.format("Task '%s' is already in status '%s'", task.getId(), status)); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/tasks/TaskWorkflowHandler.java b/openmetadata-service/src/main/java/org/openmetadata/service/tasks/TaskWorkflowHandler.java index 50a9a7db6592..380c58280df0 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/tasks/TaskWorkflowHandler.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/tasks/TaskWorkflowHandler.java @@ -232,6 +232,9 @@ private Task resolveWorkflowTask( "[TaskWorkflowHandler] Non-terminal transition '{}' for task '{}' — workflow advanced, no resolution applied", transitionId, taskId); + if (isApproveTransition(selectedTransition)) { + captureApprover(taskRepository, taskId, user); + } return refreshTask(taskId); } @@ -267,6 +270,28 @@ private Task persistWorkflowAssignees( } } + private void captureApprover(TaskRepository taskRepository, UUID taskId, String user) { + try { + EntityReference approver = + Entity.getEntityReferenceByName(Entity.USER, user, Include.NON_DELETED); + taskRepository.persistApprover(taskId, approver, user); + } catch (Exception e) { + // Pass the exception so SLF4J appends the full stack trace — losing it makes + // production approver-capture failures effectively undiagnosable. + LOG.warn("[TaskWorkflowHandler] Failed to capture approver for task '{}'", taskId, e); + } + } + + /** + * Identify an approval transition by its target status rather than its `id` string. Every + * approve transition in our seeded workflows has `targetTaskStatus=Approved`, so this avoids + * coupling the handler to the literal `"approve"` id that the workflow JSON happens to use. + */ + private static boolean isApproveTransition(TaskAvailableTransition selectedTransition) { + return selectedTransition != null + && selectedTransition.getTargetTaskStatus() == TaskEntityStatus.Approved; + } + /** * Resolve a standalone task (not managed by a workflow). */ @@ -337,6 +362,11 @@ private Task applyTaskResolution( task.setWorkflowStageId(selectedTransition.getTargetStageId()); task.setWorkflowStageDisplayName(selectedTransition.getTargetStageId()); task.setAvailableTransitions(List.of()); + if (isApproveTransition(selectedTransition)) { + task.setApprovedBy(resolvedByRef); + task.setApprovedById(resolvedByRef.getId().toString()); + task.setApprovedAt(System.currentTimeMillis()); + } } task = taskRepository.resolveTask(task, resolution, user); @@ -1169,6 +1199,27 @@ private String resolveWorkflowResult( return defaultWorkflowResult(resolutionType); } + /** + * Map a transition to a {@link TaskResolutionType} for the resolveTask path. Cascade: + * + *

      + *
    1. Caller-supplied {@code requestedResolutionType} (explicit override). + *
    2. Transition-declared {@code resolutionType} from the workflow JSON — the canonical + * signal that a transition is terminal. + *
    3. Fallback by {@code targetTaskStatus} — only for the unambiguously terminal statuses + * (Rejected, Completed, Cancelled, Revoked, Failed). {@code Approved} and + * {@code Granted} are intentionally NOT mapped here: Data Access Request (and any + * future workflow that uses Approved/Granted as a non-terminal "awaiting next step" + * state) would otherwise close the task prematurely on the approve transition. + *
    + * + *

    Convention for custom workflows: any transition that is intended to be terminal MUST + * declare an explicit {@code resolutionType} in the workflow JSON, matching the seeded + * GlossaryApproval / DescriptionUpdate / etc. definitions. Returning {@code null} here is + * the explicit signal that the transition is non-terminal — callers route through the + * workflow advancement path instead of {@code applyTaskResolution}, so the task stays + * alive on the next user-task node. + */ private TaskResolutionType resolveResolutionType( Task task, TaskResolutionType requestedResolutionType, @@ -1183,13 +1234,12 @@ private TaskResolutionType resolveResolutionType( if (selectedTransition != null && selectedTransition.getTargetTaskStatus() != null) { return switch (selectedTransition.getTargetTaskStatus()) { - case Approved -> TaskResolutionType.Approved; case Rejected -> TaskResolutionType.Rejected; case Completed -> TaskResolutionType.Completed; case Cancelled -> TaskResolutionType.Cancelled; case Revoked -> TaskResolutionType.Revoked; case Failed -> TaskResolutionType.TimedOut; - case Open, InProgress, Pending -> null; + case Open, InProgress, Pending, Approved, Granted -> null; }; } diff --git a/openmetadata-service/src/main/resources/json/data/governance/workflows/DataAccessRequestTaskWorkflow.json b/openmetadata-service/src/main/resources/json/data/governance/workflows/DataAccessRequestTaskWorkflow.json index 185039044964..76588285e174 100644 --- a/openmetadata-service/src/main/resources/json/data/governance/workflows/DataAccessRequestTaskWorkflow.json +++ b/openmetadata-service/src/main/resources/json/data/governance/workflows/DataAccessRequestTaskWorkflow.json @@ -2,7 +2,7 @@ "name": "DataAccessRequestTaskWorkflow", "fullyQualifiedName": "DataAccessRequestTaskWorkflow", "displayName": "Data Access Request Task Workflow", - "description": "Default workflow-driven lifecycle for data access request tasks. After approval, access can be revoked by the approver, owner, or domain owner.", + "description": "Default workflow-driven lifecycle for data access request tasks. After review the request moves to Approved (banner shown until access is provisioned), then to Granted once an admin marks access as provisioned. Access can later be revoked by the approver, owner, or domain owner.", "config": { "storeStageStatus": true }, @@ -40,7 +40,7 @@ "id": "approve", "label": "Approve", "targetStageId": "approved", - "targetTaskStatus": "InProgress", + "targetTaskStatus": "Approved", "formRef": "approve", "requiresComment": false }, @@ -63,7 +63,7 @@ "type": "userTask", "subType": "userApprovalTask", "name": "ApprovedAccess", - "displayName": "Active Access", + "displayName": "Approved - Awaiting Grant", "config": { "assignees": { "addReviewers": true, @@ -74,7 +74,48 @@ "rejectionThreshold": 1, "stageId": "approved", "stageDisplayName": "Approved", - "taskStatus": "InProgress", + "taskStatus": "Approved", + "assigneeStrategy": "owners-and-reviewers", + "transitionMetadata": [ + { + "id": "markAsGranted", + "label": "Mark as Granted", + "targetStageId": "granted", + "targetTaskStatus": "Granted", + "formRef": "grant", + "requiresComment": false + }, + { + "id": "revoke", + "label": "Revoke Access", + "targetStageId": "revoked", + "targetTaskStatus": "Revoked", + "resolutionType": "Revoked", + "formRef": "revoke", + "requiresComment": true + } + ] + }, + "inputNamespaceMap": { + "relatedEntity": "global" + } + }, + { + "type": "userTask", + "subType": "userApprovalTask", + "name": "GrantedAccess", + "displayName": "Active Access", + "config": { + "assignees": { + "addReviewers": true, + "addOwners": true, + "candidates": [] + }, + "approvalThreshold": 1, + "rejectionThreshold": 1, + "stageId": "granted", + "stageDisplayName": "Granted", + "taskStatus": "Granted", "assigneeStrategy": "owners-and-reviewers", "transitionMetadata": [ { @@ -122,6 +163,16 @@ }, { "from": "ApprovedAccess", + "to": "GrantedAccess", + "condition": "markAsGranted" + }, + { + "from": "ApprovedAccess", + "to": "RevokedEnd", + "condition": "revoke" + }, + { + "from": "GrantedAccess", "to": "RevokedEnd", "condition": "revoke" } diff --git a/openmetadata-service/src/main/resources/json/data/policy/DataConsumerPolicy.json b/openmetadata-service/src/main/resources/json/data/policy/DataConsumerPolicy.json index a02039cbb76e..c1a8e3c65999 100644 --- a/openmetadata-service/src/main/resources/json/data/policy/DataConsumerPolicy.json +++ b/openmetadata-service/src/main/resources/json/data/policy/DataConsumerPolicy.json @@ -13,6 +13,13 @@ "resources" : ["all"], "operations": ["ViewAll", "EditDescription", "EditTags", "EditGlossaryTerms", "EditTier", "EditCertification"], "effect": "allow" + }, + { + "name": "DataConsumerPolicy-CreateTask-Rule", + "description" : "Allow authenticated users to create tasks (data access requests, suggestions, etc.).", + "resources" : ["task"], + "operations": ["Create"], + "effect": "allow" } ] -} \ No newline at end of file +} diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/governance/workflows/elements/nodes/userTask/CreateTaskTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/governance/workflows/elements/nodes/userTask/CreateTaskTest.java index 9005cfc5c6e6..ba3e38a38684 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/governance/workflows/elements/nodes/userTask/CreateTaskTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/governance/workflows/elements/nodes/userTask/CreateTaskTest.java @@ -172,11 +172,11 @@ void testFindExistingTaskWithRetryReturnsNullAfterExhaustingWorkflowManagedLooku @Test void testIsTerminalTaskStatusReturnsTrueForResolvedStates() { - assertTrue(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Approved)); assertTrue(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Rejected)); assertTrue(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Completed)); assertTrue(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Cancelled)); assertTrue(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Failed)); + assertTrue(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Revoked)); } @Test @@ -184,6 +184,12 @@ void testIsTerminalTaskStatusReturnsFalseForOpenStates() { assertFalse(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Open)); assertFalse(CreateTask.isTerminalTaskStatus(TaskEntityStatus.InProgress)); assertFalse(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Pending)); + // Approved and Granted are non-terminal so the next-stage CreateTask listener + // (e.g. Data Access Request's ApprovedAccess → GrantedAccess advancement) can + // update status/workflowStageId/availableTransitions instead of preserving + // stale state. See the DataAccessRequestTaskWorkflow.json edges. + assertFalse(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Approved)); + assertFalse(CreateTask.isTerminalTaskStatus(TaskEntityStatus.Granted)); assertFalse(CreateTask.isTerminalTaskStatus(null)); } } diff --git a/openmetadata-spec/src/main/resources/json/schema/api/tasks/taskCount.json b/openmetadata-spec/src/main/resources/json/schema/api/tasks/taskCount.json index 1a9a6f992bb9..c859cbeb60c2 100644 --- a/openmetadata-spec/src/main/resources/json/schema/api/tasks/taskCount.json +++ b/openmetadata-spec/src/main/resources/json/schema/api/tasks/taskCount.json @@ -16,6 +16,16 @@ "type": "integer", "minimum": 0 }, + "approved": { + "description": "Total count of all tasks currently in the Approved status (across all task types). For Data Access Requests this is the 'awaiting grant' bucket; for workflows where Approved is terminal (e.g. Glossary, DescriptionUpdate) it reflects resolved tasks.", + "type": "integer", + "minimum": 0 + }, + "granted": { + "description": "Total count of all tasks currently in the Granted status. Today this status is only emitted by the Data Access Request workflow to indicate access has been provisioned and is active.", + "type": "integer", + "minimum": 0 + }, "completed": { "description": "Total count of all completed/closed tasks.", "type": "integer", diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/tasks/task.json b/openmetadata-spec/src/main/resources/json/schema/entity/tasks/task.json index 667eb1a97ef9..9968bea41b7b 100644 --- a/openmetadata-spec/src/main/resources/json/schema/entity/tasks/task.json +++ b/openmetadata-spec/src/main/resources/json/schema/entity/tasks/task.json @@ -74,6 +74,7 @@ "InProgress", "Pending", "Approved", + "Granted", "Rejected", "Completed", "Cancelled", @@ -85,6 +86,7 @@ {"name": "InProgress"}, {"name": "Pending"}, {"name": "Approved"}, + {"name": "Granted"}, {"name": "Rejected"}, {"name": "Completed"}, {"name": "Cancelled"}, @@ -319,6 +321,18 @@ "description": "UUID of the user who created this task. Stored in JSON for efficient querying via generated column index.", "type": "string" }, + "approvedBy": { + "description": "User who approved this task (set when an approval transition fires; distinct from resolution.resolvedBy which is set only on terminal transitions).", + "$ref": "../../type/entityReference.json" + }, + "approvedById": { + "description": "UUID of the user who approved this task. Stored in JSON for efficient querying via generated column index.", + "type": "string" + }, + "approvedAt": { + "description": "Timestamp when the task was approved.", + "$ref": "../../type/basic.json#/definitions/timestamp" + }, "assignees": { "description": "Users or teams assigned to complete this task.", "$ref": "../../type/entityReferenceList.json" diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx index df4f6805ff4e..2802479bdee1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx @@ -11,7 +11,16 @@ * limitations under the License. */ import Icon from '@ant-design/icons'; -import { Button, Col, Divider, Row, Space, Tooltip, Typography } from 'antd'; +import { + Alert, + Button, + Col, + Divider, + Row, + Space, + Tooltip, + Typography, +} from 'antd'; import ButtonGroup from 'antd/lib/button/button-group'; import { AxiosError } from 'axios'; import classNames from 'classnames'; @@ -162,7 +171,11 @@ export const DataAssetsHeader = ({ const { entityRules } = useEntityRules(entityType); const [dataContract, setDataContract] = useState(); const [isRequestDataAccessOpen, setIsRequestDataAccessOpen] = useState(false); - const { isDarDisabled, refetch: refetchExistingDar } = useDataAccessRequest({ + const { + isDarDisabled, + isDarAwaitingGrant, + refetch: refetchExistingDar, + } = useDataAccessRequest({ entityFqn: dataAsset.fullyQualifiedName, enabled: entityType === EntityType.TABLE, }); @@ -630,6 +643,19 @@ export const DataAssetsHeader = ({ className="data-assets-header-container" data-testid="data-assets-header" gutter={[0, 20]}> + {isDarAwaitingGrant && ( + + + + )} ({ jest.mock('../../../rest/tasksAPI', () => ({ ...jest.requireActual('../../../rest/tasksAPI'), - listTasks: jest.fn().mockResolvedValue({ data: [] }), + listDataAccessRequests: jest.fn().mockResolvedValue({ data: [] }), })); jest.mock('../../../utils/TasksUtils', () => @@ -704,10 +704,10 @@ describe('DataAssetsHeader component', () => { permissions: { ...DEFAULT_ENTITY_PERMISSION, ViewAll: true }, }; - const mockListTasks = listTasks as jest.Mock; + const mockListDataAccessRequests = listDataAccessRequests as jest.Mock; beforeEach(() => { - mockListTasks.mockResolvedValue({ data: [] }); + mockListDataAccessRequests.mockResolvedValue({ data: [] }); const { getShowRequestDataAccess } = jest.requireMock( '../../../utils/TableClassBase' ); @@ -779,7 +779,7 @@ describe('DataAssetsHeader component', () => { }); it('should render enabled button when no existing DAR task', async () => { - mockListTasks.mockResolvedValue({ data: [] }); + mockListDataAccessRequests.mockResolvedValue({ data: [] }); render(); @@ -791,7 +791,7 @@ describe('DataAssetsHeader component', () => { }); it('should disable button when a task is in review stage', async () => { - mockListTasks.mockResolvedValue({ + mockListDataAccessRequests.mockResolvedValue({ data: [ { id: 'task-1', @@ -810,7 +810,7 @@ describe('DataAssetsHeader component', () => { }); it('should disable button when task is in approved stage and approval is still active', async () => { - mockListTasks.mockResolvedValue({ + mockListDataAccessRequests.mockResolvedValue({ data: [ { id: 'task-2', @@ -831,7 +831,7 @@ describe('DataAssetsHeader component', () => { }); it('should enable button when task is in approved stage but approval has expired', async () => { - mockListTasks.mockResolvedValue({ + mockListDataAccessRequests.mockResolvedValue({ data: [ { id: 'task-3', @@ -856,7 +856,7 @@ describe('DataAssetsHeader component', () => { it('should use updatedAt (approval time) not createdAt for duration window', async () => { const approvedAt = Date.now() - 86_400_000 * 3; - mockListTasks.mockResolvedValue({ + mockListDataAccessRequests.mockResolvedValue({ data: [ { id: 'task-dur', @@ -877,7 +877,7 @@ describe('DataAssetsHeader component', () => { }); it('should treat expirationDate 0 as expired (not as missing)', async () => { - mockListTasks.mockResolvedValue({ + mockListDataAccessRequests.mockResolvedValue({ data: [ { id: 'task-zero-exp', @@ -900,7 +900,7 @@ describe('DataAssetsHeader component', () => { }); it('should disable when workflowStageDisplayName missing but workflowStageId is approved', async () => { - mockListTasks.mockResolvedValue({ + mockListDataAccessRequests.mockResolvedValue({ data: [ { id: 'task-stageid', @@ -920,7 +920,7 @@ describe('DataAssetsHeader component', () => { }); }); - it('should not call listTasks when currentUser has no name', async () => { + it('should not call listDataAccessRequests when currentUser has no name', async () => { const { useApplicationStore } = jest.requireMock( '../../../hooks/useApplicationStore' ); @@ -931,12 +931,12 @@ describe('DataAssetsHeader component', () => { render(); await waitFor(() => { - expect(mockListTasks).not.toHaveBeenCalled(); + expect(mockListDataAccessRequests).not.toHaveBeenCalled(); }); }); - it('should enable button when listTasks throws', async () => { - mockListTasks.mockRejectedValue(new Error('network error')); + it('should enable button when listDataAccessRequests throws', async () => { + mockListDataAccessRequests.mockRejectedValue(new Error('network error')); render(); diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/api/governance/createWorkflowDefinition.ts b/openmetadata-ui/src/main/resources/ui/src/generated/api/governance/createWorkflowDefinition.ts index eaefa0ec830b..8f0f1b686f00 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/api/governance/createWorkflowDefinition.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/api/governance/createWorkflowDefinition.ts @@ -244,6 +244,7 @@ export enum TaskStatus { Cancelled = "Cancelled", Completed = "Completed", Failed = "Failed", + Granted = "Granted", InProgress = "InProgress", Open = "Open", Pending = "Pending", diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/api/tasks/taskCount.ts b/openmetadata-ui/src/main/resources/ui/src/generated/api/tasks/taskCount.ts index 129aee5ac215..642309f049e5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/api/tasks/taskCount.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/api/tasks/taskCount.ts @@ -14,10 +14,22 @@ * This schema defines the type for reporting the count of tasks by status. */ export interface TaskCount { + /** + * Total count of all tasks currently in the Approved status (across all task types). For + * Data Access Requests this is the 'awaiting grant' bucket; for workflows where Approved is + * terminal (e.g. Glossary, DescriptionUpdate) it reflects resolved tasks. + */ + approved?: number; /** * Total count of all completed/closed tasks. */ completed?: number; + /** + * Total count of all tasks currently in the Granted status. Today this status is only + * emitted by the Data Access Request workflow to indicate access has been provisioned and + * is active. + */ + granted?: number; /** * Total count of all in-progress tasks. */ diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/entity/feed/taskFormSchema.ts b/openmetadata-ui/src/main/resources/ui/src/generated/entity/feed/taskFormSchema.ts index 6297c48b2752..04656ebe2a56 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/entity/feed/taskFormSchema.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/entity/feed/taskFormSchema.ts @@ -171,6 +171,7 @@ export enum TaskStatus { Cancelled = "Cancelled", Completed = "Completed", Failed = "Failed", + Granted = "Granted", InProgress = "InProgress", Open = "Open", Pending = "Pending", diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/entity/tasks/task.ts b/openmetadata-ui/src/main/resources/ui/src/generated/entity/tasks/task.ts index 9e37dae6bd75..b33b110e69a7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/entity/tasks/task.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/entity/tasks/task.ts @@ -25,6 +25,20 @@ export interface Task { * about.fullyQualifiedName using FullyQualifiedName.buildHash(). */ aboutFqnHash?: string; + /** + * Timestamp when the task was approved. + */ + approvedAt?: number; + /** + * User who approved this task (set when an approval transition fires; distinct from + * resolution.resolvedBy which is set only on terminal transitions). + */ + approvedBy?: EntityReference; + /** + * UUID of the user who approved this task. Stored in JSON for efficient querying via + * generated column index. + */ + approvedById?: string; /** * Users or teams assigned to complete this task. */ @@ -167,6 +181,9 @@ export interface Task { * example, a table has an attribute called database of type EntityReference that captures * the relationship of a table `belongs to a` database. * + * User who approved this task (set when an approval transition fires; distinct from + * resolution.resolvedBy which is set only on terminal transitions). + * * Users or teams assigned to complete this task. * * This schema defines the EntityReferenceList type used for referencing an entity. @@ -285,6 +302,7 @@ export enum TaskStatus { Cancelled = "Cancelled", Completed = "Completed", Failed = "Failed", + Granted = "Granted", InProgress = "InProgress", Open = "Open", Pending = "Pending", diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/governance/workflows/elements/nodes/userTask/userApprovalTask.ts b/openmetadata-ui/src/main/resources/ui/src/generated/governance/workflows/elements/nodes/userTask/userApprovalTask.ts index 541ddc3cc4a7..5104255c95cb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/governance/workflows/elements/nodes/userTask/userApprovalTask.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/governance/workflows/elements/nodes/userTask/userApprovalTask.ts @@ -151,6 +151,7 @@ export enum TaskStatus { Cancelled = "Cancelled", Completed = "Completed", Failed = "Failed", + Granted = "Granted", InProgress = "InProgress", Open = "Open", Pending = "Pending", diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useDataAccessRequest.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useDataAccessRequest.ts index 845359f45e4b..3ba2a381951b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/useDataAccessRequest.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useDataAccessRequest.ts @@ -12,10 +12,11 @@ */ import { useCallback, useEffect, useMemo, useState } from 'react'; import { - listTasks, + DarWorkflowStage, + listDataAccessRequests, Task, - TaskCategory, - TaskEntityType, + TaskEntityStatus, + TaskStatusGroup, } from '../rest/tasksAPI'; import { isDarApprovalActive } from '../utils/TasksUtils'; import { useApplicationStore } from './useApplicationStore'; @@ -28,6 +29,7 @@ interface UseDataAccessRequestParams { interface UseDataAccessRequestResult { isDarDisabled: boolean; + isDarAwaitingGrant: boolean; refetch: () => void; } @@ -47,11 +49,10 @@ export const useDataAccessRequest = ({ } try { - const res = await listTasks({ - aboutEntity: entityFqn, - category: TaskCategory.DataAccess, - type: TaskEntityType.DataAccessRequest, - createdBy: currentUser.name, + const res = await listDataAccessRequests({ + dataset: entityFqn, + requestedBy: currentUser.name, + statusGroup: TaskStatusGroup.Active, fields: 'about,resolution', limit: 10, }); @@ -80,33 +81,80 @@ export const useDataAccessRequest = ({ }; }, [fetchExistingDar, listenForEvents]); - const isDarDisabled = useMemo(() => { - return existingDarTasks.some((task) => { - const stage = ( - task.workflowStageDisplayName ?? - task.workflowStageId ?? - '' - ).toLowerCase(); - - if (stage === 'review') { + const isDarDisabled = useMemo( + () => + existingDarTasks.some((task) => { + // Prefer the persisted status enum (Approved/Granted carry the workflow + // semantics directly); fall back to the workflow stage name for any task + // that pre-dates the explicit status setter and only has stage metadata. + const stage = ( + task.workflowStageDisplayName ?? + task.workflowStageId ?? + '' + ).toLowerCase(); + const isApproved = + task.status === TaskEntityStatus.Approved || + stage === DarWorkflowStage.Approved; + const isGranted = + task.status === TaskEntityStatus.Granted || + stage === DarWorkflowStage.Granted; + + if (isApproved || isGranted) { + const payload = task.payload as + | { duration?: string; expirationDate?: number } + | undefined; + + // Use the persisted approvedAt for the duration window so later + // workflow transitions (e.g. granting) don't shift the apparent + // approval timestamp via updatedAt. Fall back for older tasks + // that don't carry approvedAt yet. + return isDarApprovalActive( + task.approvedAt ?? task.updatedAt ?? task.createdAt, + payload?.duration, + payload?.expirationDate + ); + } + + // The fetch is scoped to statusGroup=active, so any remaining task here + // is Open/InProgress/Pending (or stage=review) — still in-flight, block + // a duplicate request. return true; - } - - if (stage === 'approved') { + }), + [existingDarTasks] + ); + + const isDarAwaitingGrant = useMemo( + () => + existingDarTasks.some((task) => { + const stage = ( + task.workflowStageDisplayName ?? + task.workflowStageId ?? + '' + ).toLowerCase(); + const isApproved = + task.status === TaskEntityStatus.Approved || + (stage === DarWorkflowStage.Approved && + task.status !== TaskEntityStatus.Granted); + + if (!isApproved) { + return false; + } + + // Mirror the isDarApprovalActive gate used by isDarDisabled. Without this an + // expired approval would still show the "awaiting grant" banner even though + // isDarDisabled returns false (button enabled), leaving a contradictory UX. const payload = task.payload as | { duration?: string; expirationDate?: number } | undefined; return isDarApprovalActive( - task.updatedAt ?? task.createdAt, + task.approvedAt ?? task.updatedAt ?? task.createdAt, payload?.duration, payload?.expirationDate ); - } - - return false; - }); - }, [existingDarTasks]); + }), + [existingDarTasks] + ); - return { isDarDisabled, refetch: fetchExistingDar }; + return { isDarDisabled, isDarAwaitingGrant, refetch: fetchExistingDar }; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json index 321e00a25524..e356265dfd40 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json @@ -16,6 +16,7 @@ "access-policy-name": "اسم سياسة الوصول", "access-request-plural": "طلبات الوصول", "access-token": "رمز الوصول", + "access-type": "Access Type", "accessed": "تم الوصول", "account": "الحساب", "account-email": "البريد الإلكتروني للحساب", @@ -154,6 +155,7 @@ "approve": "موافقة", "approved": "تمت الموافقة", "approved-entity": "تمت الموافقة على {{entity}}", + "approver": "Approver", "approver-plural": "الموافقون", "april": "أبريل", "archived": "مؤرشف", @@ -488,6 +490,9 @@ "dashboard-plural": "لوحات المعلومات", "dashboard-service": "خدمة لوحة المعلومات", "data": "بيانات", + "data-access-request": "Data Access Request", + "data-access-request-awaiting-grant": "Approved – Awaiting Grant", + "data-access-request-plural": "Data Access Requests", "data-aggregate": "تجميع البيانات", "data-aggregation": "تجميع البيانات", "data-asset": "أصل البيانات", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "URI الرمز المميز لـ Google Cloud.", "govern": "حوكمة", "governance": "الحوكمة", + "grant-access": "Grant Access", + "granted": "Granted", "granularity": "دقة التفاصيل", "graph": "رسم بياني", "graph-settings": "إعدادات الرسم البياني", @@ -1255,6 +1262,7 @@ "many-to-one": "متعدد إلى واحد", "march": "مارس", "mark-all-deleted-table-plural": "وضع علامة على جميع الجداول المحذوفة", + "mark-as-granted": "Mark as Granted", "mark-deleted-entity": "وضع علامة على {{entity}} المحذوف", "mark-deleted-table-plural": "وضع علامة على الجداول المحذوفة", "markdown-guide": "دليل Markdown", @@ -1748,6 +1756,7 @@ "request-method": "طريقة الطلب", "request-schema-field": "حقل مخطط الطلب", "request-tag-plural": "طلب وسوم", + "requested-by": "Requested By", "required": "مطلوب", "requirement-plural": "متطلبات", "reset": "إعادة تعيين", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "محذوف ناعم", "soft-lowercase": "ناعم", "sort": "ترتيب", + "sort-ascending": "Ascending", "sort-by-field": "ترتيب حسب {{field}}", + "sort-descending": "Descending", + "sort-order": "Sort Order", "source": "المصدر", "source-aligned": "مُحاذٍ للمصدر", "source-column": "عمود المصدر", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "تخصيص تجربة صفحة كيان {{entity}} لشخصية <0>{{persona}}", "customize-home-page-page-header-for-persona": "تخصيص تجربة الصفحة الرئيسية لشخصية <0>{{persona}}", "customize-your-navigation-subheader": "إدارة وتنظيم قائمة التنقل الجانبية لتحسين إمكانية الوصول.", + "data-access-request-already-exists": "A pending data access request already exists for this asset.", + "data-access-request-awaiting-grant-message": "Your access request has been approved and is awaiting provisioning. Access will be available shortly once it has been granted.", "data-access-request-message": "طلب الوصول إلى البيانات لـ", "data-asset-has-been-action-type": "تم {{actionType}} أصل البيانات", "data-asset-rules-message": "تساعدك قواعد أصول البيانات على إدارة عمليات التحقق من صحة البيانات الوصفية على مستوى المنصة. إذا لم يتبع أحد الأصول قاعدة ما، فلن تتمكن من إجراء أي تغييرات عليه ما لم تقم بإصلاح التحقق من الصحة أولاً.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json index 6d60b8226d10..24a364c8712d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json @@ -16,6 +16,7 @@ "access-policy-name": "Name der Zugangsrichtlinie", "access-request-plural": "Zugriffsanfragen", "access-token": "Zugangstoken", + "access-type": "Zugriffstyp", "accessed": "Zugegriffen", "account": "Konto", "account-email": "Konto-E-Mail", @@ -154,6 +155,7 @@ "approve": "Genehmigen", "approved": "Genehmigt", "approved-entity": "Genehmigte {{entity}}", + "approver": "Genehmiger", "approver-plural": "Genehmiger", "april": "April", "archived": "Archiviert", @@ -488,6 +490,9 @@ "dashboard-plural": "Dashboards", "dashboard-service": "Dashboard-Dienst", "data": "Daten", + "data-access-request": "Datenzugriffsanfrage", + "data-access-request-awaiting-grant": "Genehmigt – Ausstehende Gewährung", + "data-access-request-plural": "Datenzugriffsanfragen", "data-aggregate": "Datenaggregat", "data-aggregation": "Datenaggregation", "data-asset": "Daten-Asset", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "Google Cloud Token-URI.", "govern": "Verwalten", "governance": "Regierungsführung", + "grant-access": "Zugriff gewähren", + "granted": "Gewährt", "granularity": "Granularität", "graph": "Graph", "graph-settings": "Grapheinstellungen", @@ -1255,6 +1262,7 @@ "many-to-one": "Viele zu Eins", "march": "März", "mark-all-deleted-table-plural": "Alle gelöschten Tabellen markieren", + "mark-as-granted": "Als gewährt markieren", "mark-deleted-entity": "{{entity}} als gelöscht markieren", "mark-deleted-table-plural": "Gelöschte Tabellen markieren", "markdown-guide": "Markdown-Leitfaden", @@ -1748,6 +1756,7 @@ "request-method": "Anfrage Methode", "request-schema-field": "Anfrage Feld Schema", "request-tag-plural": "Tag-Anfragen", + "requested-by": "Angefordert von", "required": "Erforderlich", "requirement-plural": "Anforderungen", "reset": "Zurücksetzen", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "soft-gelöscht", "soft-lowercase": "weich", "sort": "Sortieren", + "sort-ascending": "Aufsteigend", "sort-by-field": "Sortieren nach {{field}}", + "sort-descending": "Absteigend", + "sort-order": "Sortierreihenfolge", "source": "Quelle", "source-aligned": "Quellenorientiert", "source-column": "Quellenspalte", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "Personalisieren Sie die {{entity}}-Entitätsseite für die <0>{{persona}}-Persona", "customize-home-page-page-header-for-persona": "Personalisieren Sie die Homepage-Erfahrung für die <0>{{persona}} Persona", "customize-your-navigation-subheader": "Verwalten und organisieren Sie das Seitennavigationsmenü für bessere Zugänglichkeit.", + "data-access-request-already-exists": "Für dieses Asset existiert bereits eine ausstehende Datenzugriffsanfrage.", + "data-access-request-awaiting-grant-message": "Ihre Zugriffsanfrage wurde genehmigt und wartet auf die Bereitstellung. Der Zugriff wird in Kürze verfügbar sein, sobald er gewährt wurde.", "data-access-request-message": "Datenzugriffsanfrage für", "data-asset-has-been-action-type": "Der Datenvermögenswert wurde {{actionType}}", "data-asset-rules-message": "Datenasset-Regeln helfen Ihnen dabei, Metadaten-Validierungen auf Plattformebene zu verwalten. Wenn ein Asset einer Regel nicht folgt, können Sie keine Änderungen daran vornehmen, bis Sie die Validierung zuerst beheben.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index 638f850de99a..8affd3ea4a6b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -16,6 +16,7 @@ "access-policy-name": "Access Policy Name", "access-request-plural": "Access Requests", "access-token": "Access Token", + "access-type": "Access Type", "accessed": "Accessed", "account": "Account", "account-email": "Account email", @@ -154,6 +155,7 @@ "approve": "Approve", "approved": "Approved", "approved-entity": "Approved {{entity}}", + "approver": "Approver", "approver-plural": "Approvers", "april": "April", "archived": "Archived", @@ -488,6 +490,9 @@ "dashboard-plural": "Dashboards", "dashboard-service": "Dashboard Service", "data": "Data", + "data-access-request": "Data Access Request", + "data-access-request-awaiting-grant": "Approved – Awaiting Grant", + "data-access-request-plural": "Data Access Requests", "data-aggregate": "Data Aggregate", "data-aggregation": "Data Aggregation", "data-asset": "Data Asset", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "Google Cloud token uri.", "govern": "Govern", "governance": "Governance", + "grant-access": "Grant Access", + "granted": "Granted", "granularity": "Granularity", "graph": "Graph", "graph-settings": "Graph Settings", @@ -1255,6 +1262,7 @@ "many-to-one": "Many to One", "march": "March", "mark-all-deleted-table-plural": "Mark All Deleted Tables", + "mark-as-granted": "Mark as Granted", "mark-deleted-entity": "Mark Deleted {{entity}}", "mark-deleted-table-plural": "Mark Deleted Tables", "markdown-guide": "Markdown Guide", @@ -1748,6 +1756,7 @@ "request-method": "Request Method", "request-schema-field": "Request Schema Field", "request-tag-plural": "Request Tags", + "requested-by": "Requested By", "required": "Required", "requirement-plural": "Requirements", "reset": "Reset", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "soft deleted", "soft-lowercase": "soft", "sort": "Sort", + "sort-ascending": "Ascending", "sort-by-field": "Sort by {{field}}", + "sort-descending": "Descending", + "sort-order": "Sort Order", "source": "Source", "source-aligned": "Source-aligned", "source-column": "Source Column", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "Personalize the {{entity}} Entity Page experience for the <0>{{persona}} persona", "customize-home-page-page-header-for-persona": "Personalize the Home page experience for the <0>{{persona}} persona", "customize-your-navigation-subheader": "Manage and organize the side navigation menu for better accessibility.", + "data-access-request-already-exists": "A pending data access request already exists for this asset.", + "data-access-request-awaiting-grant-message": "Your access request has been approved and is awaiting provisioning. Access will be available shortly once it has been granted.", "data-access-request-message": "Data access request for", "data-asset-has-been-action-type": "Data Asset has been {{actionType}}", "data-asset-rules-message": "Data Asset Rules help you manage metadata validations at the platform level. If an asset does not follow a rule, you won't be able to make any changes to it unless you fix the validation first.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json index 6b5e462cf61d..7444a5d20e22 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json @@ -16,6 +16,7 @@ "access-policy-name": "Nombre de la Política de Acceso", "access-request-plural": "Solicitudes de acceso", "access-token": "Token de Acceso", + "access-type": "Tipo de acceso", "accessed": "Accedido", "account": "Cuenta", "account-email": "Correo electrónico de la cuenta", @@ -154,6 +155,7 @@ "approve": "Aprobar", "approved": "Aprobado", "approved-entity": "Entidad aprobada {{entity}}", + "approver": "Aprobador", "approver-plural": "Aprobadores", "april": "Abril", "archived": "Archivado", @@ -488,6 +490,9 @@ "dashboard-plural": "Paneles", "dashboard-service": "Servicio de Panel", "data": "Datos", + "data-access-request": "Solicitud de acceso a datos", + "data-access-request-awaiting-grant": "Aprobado – Pendiente de concesión", + "data-access-request-plural": "Solicitudes de acceso a datos", "data-aggregate": "Agregado de Datos", "data-aggregation": "Agregación de Datos", "data-asset": "Activo de datos", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "URI del token de Google Cloud.", "govern": "Gobernar", "governance": "Gobernanza", + "grant-access": "Otorgar acceso", + "granted": "Concedido", "granularity": "Granularidad", "graph": "Grafo", "graph-settings": "Configuración del Grafo", @@ -1255,6 +1262,7 @@ "many-to-one": "Muchos a Uno", "march": "Marzo", "mark-all-deleted-table-plural": "Marcar Todas las Tablas como Eliminadas", + "mark-as-granted": "Marcar como concedido", "mark-deleted-entity": "Marcar {{entity}} como Eliminado", "mark-deleted-table-plural": "Marcar Tablas como Eliminadas", "markdown-guide": "Guía de Markdown", @@ -1748,6 +1756,7 @@ "request-method": "Petición Método", "request-schema-field": "Petición Campo del Esquema", "request-tag-plural": "Etiquetas de solicitud", + "requested-by": "Solicitado por", "required": "Requerida", "requirement-plural": "Requisitos", "reset": "Restablecer", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "borrado suave", "soft-lowercase": "suave", "sort": "Ordenar", + "sort-ascending": "Ascendente", "sort-by-field": "Ordenar por {{field}}", + "sort-descending": "Descendente", + "sort-order": "Orden de clasificación", "source": "Fuente", "source-aligned": "Alineado con el Origen", "source-column": "Columna de Origen", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "Personaliza la experiencia de la Página de Entidad {{entity}} para la persona <0>{{persona}}", "customize-home-page-page-header-for-persona": "Personaliza la experiencia de la página de inicio para la <0>{{persona}} persona", "customize-your-navigation-subheader": "Gestione y organice el menu de navegación lateral para una mejor accesibilidad.", + "data-access-request-already-exists": "Ya existe una solicitud de acceso a datos pendiente para este recurso.", + "data-access-request-awaiting-grant-message": "Su solicitud de acceso ha sido aprobada y está pendiente de aprovisionamiento. El acceso estará disponible en breve una vez que se haya concedido.", "data-access-request-message": "Solicitud de acceso a datos para", "data-asset-has-been-action-type": "El activo de datos ha sido {{actionType}}", "data-asset-rules-message": "Configure las Reglas de los Activos de Datos.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index 3a060ca5c996..1db24c205202 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -16,6 +16,7 @@ "access-policy-name": "Nom de la Politique d'Accès", "access-request-plural": "Demandes d'accès", "access-token": "Jeton d'Accès", + "access-type": "Type d'accès", "accessed": "Accédé", "account": "Compte", "account-email": "Compte email", @@ -154,6 +155,7 @@ "approve": "Approuver", "approved": "Approuvé", "approved-entity": "Entité approuvée {{entity}}", + "approver": "Approbateur", "approver-plural": "Approbateurs", "april": "Avril", "archived": "Archivé", @@ -488,6 +490,9 @@ "dashboard-plural": "Tableaux de Bord", "dashboard-service": "Service de Tableau de Bord", "data": "Données", + "data-access-request": "Demande d'accès aux données", + "data-access-request-awaiting-grant": "Approuvé – En attente d'octroi", + "data-access-request-plural": "Demandes d'accès aux données", "data-aggregate": "Agrégat de Données", "data-aggregation": "Agrégation de Données", "data-asset": "Actif de Données", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "URI du Token Google Cloud.", "govern": "Gouverner", "governance": "Gouvernance", + "grant-access": "Accorder l'accès", + "granted": "Accordé", "granularity": "Granularité", "graph": "Graphe", "graph-settings": "Paramètres du Graphe", @@ -1255,6 +1262,7 @@ "many-to-one": "Plusieurs à Un", "march": "Mars", "mark-all-deleted-table-plural": "Marquer Toutes les Tables Supprimées", + "mark-as-granted": "Marquer comme accordé", "mark-deleted-entity": "Marquer {{entity}} Supprimé·e", "mark-deleted-table-plural": "Marquer les Tables Supprimé·e", "markdown-guide": "Guide Markdown", @@ -1748,6 +1756,7 @@ "request-method": "Méthode de Requête", "request-schema-field": "Champ de Schéma de Requête", "request-tag-plural": "Demander l'ajout de tags", + "requested-by": "Demandé par", "required": "Requis", "requirement-plural": "Critères", "reset": "Réinitialiser", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "logiciel supprimé", "soft-lowercase": "logique (soft)", "sort": "Trier", + "sort-ascending": "Croissant", "sort-by-field": "Trier par {{field}}", + "sort-descending": "Décroissant", + "sort-order": "Ordre de tri", "source": "Source", "source-aligned": "Aligné source", "source-column": "Colonne Source", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "Personnalisez l'expérience de la page d'entité {{entity}} pour la persona <0>{{persona}}", "customize-home-page-page-header-for-persona": "Personnalisez l'expérience de la page d'accueil pour la <0>{{persona}} persona", "customize-your-navigation-subheader": "Gérez et organisez le menu de navigation latérale pour une meilleure accessibilité.", + "data-access-request-already-exists": "Une demande d'accès aux données en attente existe déjà pour cette ressource.", + "data-access-request-awaiting-grant-message": "Votre demande d'accès a été approuvée et est en attente de provisionnement. L'accès sera disponible sous peu une fois qu'il aura été accordé.", "data-access-request-message": "Demande d'accès aux données pour", "data-asset-has-been-action-type": "l'actif de données a été {{actionType}}", "data-asset-rules-message": "Les Règles d'Actifs de Données vous aident à gérer les validations de métadonnées au niveau de la plateforme. Si un actif ne suit pas une règle, vous ne pourrez pas y apporter de modifications tant que vous n'aurez pas corrigé la validation en premier.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json index 9409c1e48145..cb441b84f0d2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json @@ -16,6 +16,7 @@ "access-policy-name": "Nome da Política de Acceso", "access-request-plural": "Solicitudes de acceso", "access-token": "Token de Acceso", + "access-type": "Access Type", "accessed": "Accedido", "account": "Conta", "account-email": "Correo electrónico da conta", @@ -154,6 +155,7 @@ "approve": "Aprobar", "approved": "Aprobado", "approved-entity": "Entidade aprobada {{entity}}", + "approver": "Approver", "approver-plural": "Aprobadores", "april": "Abril", "archived": "Arquivado", @@ -488,6 +490,9 @@ "dashboard-plural": "Paneis", "dashboard-service": "Servizo de Panel de Control", "data": "Datos", + "data-access-request": "Data Access Request", + "data-access-request-awaiting-grant": "Approved – Awaiting Grant", + "data-access-request-plural": "Data Access Requests", "data-aggregate": "Datos agregados", "data-aggregation": "Agregación de datos", "data-asset": "Activo de datos", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "URI do token de Google Cloud.", "govern": "Gobernar", "governance": "Gobernanza", + "grant-access": "Grant Access", + "granted": "Granted", "granularity": "Granularidade", "graph": "Grafo", "graph-settings": "Configuración do Grafo", @@ -1255,6 +1262,7 @@ "many-to-one": "Moitos a Un", "march": "Marzo", "mark-all-deleted-table-plural": "Marcar todas as táboas eliminadas", + "mark-as-granted": "Mark as Granted", "mark-deleted-entity": "Marcar {{entity}} como eliminado", "mark-deleted-table-plural": "Marcar táboas como eliminadas", "markdown-guide": "Guía de Markdown", @@ -1748,6 +1756,7 @@ "request-method": "Método de solicitude", "request-schema-field": "Campo de esquema de solicitude", "request-tag-plural": "Solicitar etiquetas", + "requested-by": "Requested By", "required": "Obrigatorio", "requirement-plural": "Requisitos", "reset": "Restablecer", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "eliminado suavemente", "soft-lowercase": "suave", "sort": "Ordenar", + "sort-ascending": "Ascending", "sort-by-field": "Ordenar por {{field}}", + "sort-descending": "Descending", + "sort-order": "Sort Order", "source": "Fonte", "source-aligned": "Aliñado coa fonte", "source-column": "Columna de orixe", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "Personaliza a experiencia da páxina de entidade {{entity}} para a persoa <0>{{persona}}", "customize-home-page-page-header-for-persona": "Personaliza a experiencia da páxina de inicio para a <0>{{persona}} persoa", "customize-your-navigation-subheader": "Xestiona e organiza o menú lateral de navegación para unha mellor accesibilidade.", + "data-access-request-already-exists": "A pending data access request already exists for this asset.", + "data-access-request-awaiting-grant-message": "Your access request has been approved and is awaiting provisioning. Access will be available shortly once it has been granted.", "data-access-request-message": "Solicitude de acceso a datos para", "data-asset-has-been-action-type": "O activo de datos foi {{actionType}}", "data-asset-rules-message": "As Regras de Activos de Datos axúdanche a xestionar validacións de metadatos ao nivel da plataforma. Se un activo non segue unha regra, non poderás facer cambios nel ata que soluciones a validación primeiro.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json index 2f1d59443bc9..4f34033f52d6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json @@ -16,6 +16,7 @@ "access-policy-name": "שם מדיניות הגישה", "access-request-plural": "בקשות גישה", "access-token": "אסימון גישה", + "access-type": "סוג גישה", "accessed": "נגיש", "account": "חשבון", "account-email": "אימייל חשבון", @@ -154,6 +155,7 @@ "approve": "אשר", "approved": "אושר", "approved-entity": "{{entity}} מאושר", + "approver": "מאשר", "approver-plural": "מאשרים", "april": "אפריל", "archived": "בארכיון", @@ -488,6 +490,9 @@ "dashboard-plural": "דשבורדים", "dashboard-service": "שירות לוח מחוונים", "data": "נתונים", + "data-access-request": "בקשת גישה לנתונים", + "data-access-request-awaiting-grant": "אושר – ממתין להענקה", + "data-access-request-plural": "בקשות גישה לנתונים", "data-aggregate": "אגרגציית נתונים", "data-aggregation": "אגרגציות נתונים", "data-asset": "נכס נתונים", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "URI טוקן Google Cloud.", "govern": "ממשל", "governance": "משילות נתונים", + "grant-access": "הענק גישה", + "granted": "הוענק", "granularity": "רמת פירוט", "graph": "גרף", "graph-settings": "הגדרות גרף", @@ -1255,6 +1262,7 @@ "many-to-one": "רבים לאחד", "march": "מרץ", "mark-all-deleted-table-plural": "סמן את כל הטבלאות שנמחקו", + "mark-as-granted": "סמן כהוענק", "mark-deleted-entity": "סמן {{entity}} שנמחק", "mark-deleted-table-plural": "סמן טבלאות שנמחקו", "markdown-guide": "מדריך Markdown", @@ -1748,6 +1756,7 @@ "request-method": "שיטת בקשה", "request-schema-field": "שדה סכמת בקשה", "request-tag-plural": "בקשת תגיות", + "requested-by": "מבוקש על ידי", "required": "נדרש", "requirement-plural": "דרישות", "reset": "איפוס", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "נמחק רך", "soft-lowercase": "רך", "sort": "מיון", + "sort-ascending": "עולה", "sort-by-field": "מיין לפי {{field}}", + "sort-descending": "יורד", + "sort-order": "סדר מיון", "source": "מקור", "source-aligned": "מקור מתואם", "source-column": "עמודת מקור", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "התאם אישית את חוויית דף הישות של {{entity}} עבור הפרסונה <0>{{persona}}", "customize-home-page-page-header-for-persona": "התאמה אישית של חוויית דף הבית עבור <0>{{persona}} פרסונה", "customize-your-navigation-subheader": "נהל וארגן את תפריט הניווט הצדדי לנגישות טובה יותר.", + "data-access-request-already-exists": "קיימת כבר בקשת גישה לנתונים ממתינה עבור נכס זה.", + "data-access-request-awaiting-grant-message": "בקשת הגישה שלך אושרה וממתינה להקצאה. הגישה תהיה זמינה בקרוב לאחר שתוענק.", "data-access-request-message": "בקשת גישה לנתונים עבור", "data-asset-has-been-action-type": "נכנס של {{actionType}} נתונים", "data-asset-rules-message": "כללי נכסי נתונים עוזרים לכם לנהל אימותי מטא-נתונים ברמת הפלטפורמה. אם נכס לא עומד בכלל, לא תוכלו לבצע בו שינויים עד שתתקנו את האימות תחילה.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json index 3fa8137721a5..65a4eb9b7673 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json @@ -16,6 +16,7 @@ "access-policy-name": "アクセス ポリシー名", "access-request-plural": "アクセスリクエスト", "access-token": "アクセストークン", + "access-type": "アクセスタイプ", "accessed": "アクセス済み", "account": "アカウント", "account-email": "アカウントのEmail", @@ -154,6 +155,7 @@ "approve": "承認", "approved": "承認済み", "approved-entity": "承認された {{entity}}", + "approver": "承認者", "approver-plural": "承認者", "april": "4月", "archived": "アーカイブ済み", @@ -488,6 +490,9 @@ "dashboard-plural": "ダッシュボード", "dashboard-service": "ダッシュボードサービス", "data": "データ", + "data-access-request": "データアクセスリクエスト", + "data-access-request-awaiting-grant": "承認済み – 付与待ち", + "data-access-request-plural": "データアクセスリクエスト", "data-aggregate": "データ集約", "data-aggregation": "データ集計", "data-asset": "データアセット", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "Google Cloud トークンURI", "govern": "管理", "governance": "ガバナンス", + "grant-access": "アクセスを付与", + "granted": "付与済み", "granularity": "粒度", "graph": "グラフ", "graph-settings": "グラフ設定", @@ -1255,6 +1262,7 @@ "many-to-one": "多対一", "march": "3月", "mark-all-deleted-table-plural": "すべての削除済みテーブルをマーク", + "mark-as-granted": "付与済みとしてマーク", "mark-deleted-entity": "削除済み{{entity}}をマーク", "mark-deleted-table-plural": "削除されたテーブルをマーク", "markdown-guide": "Markdownガイド", @@ -1748,6 +1756,7 @@ "request-method": "リクエストメソッド", "request-schema-field": "リクエストスキーマフィールド", "request-tag-plural": "タグをリクエスト", + "requested-by": "リクエスト者", "required": "必須", "requirement-plural": "要件", "reset": "リセット", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "ソフト削除", "soft-lowercase": "ソフト", "sort": "並べ替え", + "sort-ascending": "昇順", "sort-by-field": "{{field}} で並べ替え", + "sort-descending": "降順", + "sort-order": "並び順", "source": "ソース", "source-aligned": "ソース整列", "source-column": "ソースカラム", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "{{entity}} エンティティページの体験を <0>{{persona}} ペルソナ向けにパーソナライズします", "customize-home-page-page-header-for-persona": "<0>{{persona}} ペルソナのホームページ体験をパーソナライズします", "customize-your-navigation-subheader": "サイドナビゲーションメニューを整理して、アクセシビリティを向上させましょう。", + "data-access-request-already-exists": "このアセットに対する保留中のデータアクセスリクエストが既に存在します。", + "data-access-request-awaiting-grant-message": "アクセスリクエストが承認され、プロビジョニング待ちの状態です。付与されると間もなくアクセスが利用可能になります。", "data-access-request-message": "データアクセスリクエスト:", "data-asset-has-been-action-type": "データアセットが {{actionType}} されました", "data-asset-rules-message": "データアセットルールを構成する", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json index d3b2bd67b261..eabb3ced58a0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json @@ -16,6 +16,7 @@ "access-policy-name": "접근 정책 이름", "access-request-plural": "액세스 요청", "access-token": "접근 토큰", + "access-type": "Access Type", "accessed": "접근됨", "account": "계정", "account-email": "계정 이메일", @@ -154,6 +155,7 @@ "approve": "승인", "approved": "승인됨", "approved-entity": "승인된 {{entity}}", + "approver": "Approver", "approver-plural": "승인자", "april": "4월", "archived": "보관됨", @@ -488,6 +490,9 @@ "dashboard-plural": "대시보드들", "dashboard-service": "대시보드 서비스", "data": "데이터", + "data-access-request": "Data Access Request", + "data-access-request-awaiting-grant": "Approved – Awaiting Grant", + "data-access-request-plural": "Data Access Requests", "data-aggregate": "데이터 집계", "data-aggregation": "데이터 집계", "data-asset": "데이터 자산", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "구글 클라우드 토큰 URI", "govern": "관리", "governance": "거버넌스", + "grant-access": "Grant Access", + "granted": "Granted", "granularity": "세분성", "graph": "그래프", "graph-settings": "그래프 설정", @@ -1255,6 +1262,7 @@ "many-to-one": "다대일", "march": "3월", "mark-all-deleted-table-plural": "모든 삭제된 테이블 표시", + "mark-as-granted": "Mark as Granted", "mark-deleted-entity": "삭제된 {{entity}} 표시", "mark-deleted-table-plural": "삭제된 테이블 표시", "markdown-guide": "마크다운 가이드", @@ -1748,6 +1756,7 @@ "request-method": "요청 메서드", "request-schema-field": "요청 스키마 필드", "request-tag-plural": "태그 요청", + "requested-by": "Requested By", "required": "필수", "requirement-plural": "요구사항들", "reset": "초기화", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "소프트 삭제됨", "soft-lowercase": "소프트", "sort": "정렬", + "sort-ascending": "Ascending", "sort-by-field": "{{field}}별 정렬", + "sort-descending": "Descending", + "sort-order": "Sort Order", "source": "소스", "source-aligned": "소스 정렬", "source-column": "소스 열", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "<0>{{persona}} 페르소나를 위한 {{entity}} 엔티티 페이지 경험을 개인화하세요.", "customize-home-page-page-header-for-persona": "<0>{{persona}} 페르소나를 위한 홈페이지 경험을 개인화하세요", "customize-your-navigation-subheader": "더 나은 접근성을 위해 사이드 내비게이션 메뉴를 관리하고 구성하세요.", + "data-access-request-already-exists": "A pending data access request already exists for this asset.", + "data-access-request-awaiting-grant-message": "Your access request has been approved and is awaiting provisioning. Access will be available shortly once it has been granted.", "data-access-request-message": "데이터 액세스 요청 대상:", "data-asset-has-been-action-type": "데이터 자산이 {{actionType}}되었습니다", "data-asset-rules-message": "데이터 자산 규칙은 플랫폼 레벨에서 메타데이터 검증을 관리하는 데 도움이 됩니다. 자산이 규칙을 따르지 않으면 먼저 검증을 수정하지 않는 한 변경할 수 없습니다.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json index 8297f4701018..3dcf70dd57fb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json @@ -16,6 +16,7 @@ "access-policy-name": "प्रवेश धोरणाचे नाव", "access-request-plural": "अॅक्सेस विनंत्या", "access-token": "प्रवेश टोकन", + "access-type": "Access Type", "accessed": "प्रवेश केला", "account": "खाते", "account-email": "खाते ईमेल", @@ -154,6 +155,7 @@ "approve": "मंजूर", "approved": "मंजूर केले", "approved-entity": "अधिकृत {{entity}}", + "approver": "Approver", "approver-plural": "मंजूरीदार", "april": "एप्रिल", "archived": "संग्रहित", @@ -488,6 +490,9 @@ "dashboard-plural": "डॅशबोर्ड्स", "dashboard-service": "डॅशबोर्ड सेवा", "data": "डेटा", + "data-access-request": "Data Access Request", + "data-access-request-awaiting-grant": "Approved – Awaiting Grant", + "data-access-request-plural": "Data Access Requests", "data-aggregate": "डेटा एकत्रीकरण", "data-aggregation": "डेटा संकलन", "data-asset": "डेटा ॲसेट", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "गूगल क्लाउड टोकन यूआरआय.", "govern": "शासन करा", "governance": "शासन", + "grant-access": "Grant Access", + "granted": "Granted", "granularity": "सूक्ष्मता", "graph": "आलेख", "graph-settings": "आलेख सेटिंग्ज", @@ -1255,6 +1262,7 @@ "many-to-one": "अनेक ते एक", "march": "मार्च", "mark-all-deleted-table-plural": "सर्व मिटवलेली टेबल्स चिन्हांकित करा", + "mark-as-granted": "Mark as Granted", "mark-deleted-entity": "मिटवलेले {{entity}} चिन्हांकित करा", "mark-deleted-table-plural": "मिटवलेली टेबल्स चिन्हांकित करा", "markdown-guide": "मार्कडाउन मार्गदर्शक", @@ -1748,6 +1756,7 @@ "request-method": "विनंती पद्धत", "request-schema-field": "विनंती स्कीमा फील्ड", "request-tag-plural": "विनंती टॅग", + "requested-by": "Requested By", "required": "आवश्यक", "requirement-plural": "आवश्यकता", "reset": "रीसेट", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "सॉफ्ट डिलीट केलेले", "soft-lowercase": "सॉफ्ट", "sort": "वर्गीकरण", + "sort-ascending": "Ascending", "sort-by-field": "{{field}} नुसार क्रमवारी लावा", + "sort-descending": "Descending", + "sort-order": "Sort Order", "source": "स्रोत", "source-aligned": "स्रोत-संरेखित", "source-column": "स्रोत स्तंभ", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "{{persona}} व्यक्तिमत्वासाठी {{entity}} घटक पृष्ठ अनुभव वैयक्तिकृत करा", "customize-home-page-page-header-for-persona": "<0>{{persona}} पर्सोनासाठी होमपेज अनुभव वैयक्तिकृत करा", "customize-your-navigation-subheader": "चांगल्या प्रवेशयोग्यतेसाठी साइड नेव्हिगेशन मेनू व्यवस्थापित करा आणि व्यवस्थित करा.", + "data-access-request-already-exists": "A pending data access request already exists for this asset.", + "data-access-request-awaiting-grant-message": "Your access request has been approved and is awaiting provisioning. Access will be available shortly once it has been granted.", "data-access-request-message": "डेटा प्रवेश विनंती साठी", "data-asset-has-been-action-type": "डेटा ॲसेट {{actionType}} केले आहे", "data-asset-rules-message": "डेटा अ‍ॅसेट नियम आपल्याला प्लेटफॉर्म स्तरावर मेटाडेटा प्रमाणीकरण व्यवस्थापित करण्यास मदत करतात. जर एखादी संपत्ती नियमाचे पालन करत नसेल, तर आपण प्रथम प्रमाणीकरण दुरुस्त केल्याशिवाय त्यात कोणतेही बदल करू शकणार नाही.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json index 8a7c3da5bfc7..84122c5ca459 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json @@ -16,6 +16,7 @@ "access-policy-name": "Naam Toegangsbeleid", "access-request-plural": "Toegangsverzoeken", "access-token": "Toegangstoken", + "access-type": "Access Type", "accessed": "Toegang verkregen", "account": "Rekening", "account-email": "Account e-mail", @@ -154,6 +155,7 @@ "approve": "Goedkeuren", "approved": "Goedgekeurd", "approved-entity": "{{entity}} goedgekeurd", + "approver": "Approver", "approver-plural": "Goedkeurders", "april": "April", "archived": "Gearchiveerd", @@ -488,6 +490,9 @@ "dashboard-plural": "Dashboards", "dashboard-service": "Dashboard-service", "data": "Gegevens", + "data-access-request": "Data Access Request", + "data-access-request-awaiting-grant": "Approved – Awaiting Grant", + "data-access-request-plural": "Data Access Requests", "data-aggregate": "Data-aggregaat", "data-aggregation": "Data-aggregatie", "data-asset": "Data-asset", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "Token-uri van Google Cloud.", "govern": "Beheer", "governance": "Governance", + "grant-access": "Grant Access", + "granted": "Granted", "granularity": "Granulariteit", "graph": "Graf", "graph-settings": "Grafinstellingen", @@ -1255,6 +1262,7 @@ "many-to-one": "Veel naar één", "march": "maart", "mark-all-deleted-table-plural": "Markeer alle verwijderde tabellen", + "mark-as-granted": "Mark as Granted", "mark-deleted-entity": "Markeer verwijderde {{entity}}", "mark-deleted-table-plural": "Markeer verwijderde tabellen", "markdown-guide": "Markdown-handleiding", @@ -1748,6 +1756,7 @@ "request-method": "Requestmethode", "request-schema-field": "Requestschemaveld", "request-tag-plural": "Aanvraagtags", + "requested-by": "Requested By", "required": "Verplicht", "requirement-plural": "Vereisten", "reset": "Herstellen", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "zacht verwijderd", "soft-lowercase": "zacht", "sort": "Sorteren", + "sort-ascending": "Ascending", "sort-by-field": "Sorteren op {{field}}", + "sort-descending": "Descending", + "sort-order": "Sort Order", "source": "Bron", "source-aligned": "Bron-georiënteerd", "source-column": "Bronkolom", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "Personaliseer de {{entity}} entiteitspagina-ervaring voor de <0>{{persona}} persona", "customize-home-page-page-header-for-persona": "Personaliseer de Home page-ervaring voor de <0>{{persona}} persona", "customize-your-navigation-subheader": "Beheer en organiseer het zijnavigatiemenu voor betere toegankelijkheid.", + "data-access-request-already-exists": "A pending data access request already exists for this asset.", + "data-access-request-awaiting-grant-message": "Your access request has been approved and is awaiting provisioning. Access will be available shortly once it has been granted.", "data-access-request-message": "Verzoek tot datatoegang voor", "data-asset-has-been-action-type": "Data-asset is {{actionType}}", "data-asset-rules-message": "Data Asset Regels helpen je bij het beheren van metadata-validaties op platformniveau. Als een asset een regel niet volgt, kun je er geen wijzigingen in aanbrengen tenzij je eerst de validatie herstelt.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json index 2b227d1ea315..6cc6a4e07dca 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json @@ -16,6 +16,7 @@ "access-policy-name": "نام سیاست دسترسی", "access-request-plural": "درخواست‌های دسترسی", "access-token": "توکن دسترسی", + "access-type": "Access Type", "accessed": "دسترسی پیدا کرد", "account": "حساب", "account-email": "ایمیل حساب", @@ -154,6 +155,7 @@ "approve": "تأیید", "approved": "تأیید شد", "approved-entity": "{{entity}} تایید شده", + "approver": "Approver", "approver-plural": "تأییدکنندگان", "april": "آوریل", "archived": "بایگانی شده", @@ -488,6 +490,9 @@ "dashboard-plural": "داشبوردها", "dashboard-service": "سرویس داشبورد", "data": "داده", + "data-access-request": "Data Access Request", + "data-access-request-awaiting-grant": "Approved – Awaiting Grant", + "data-access-request-plural": "Data Access Requests", "data-aggregate": "تجمیع داده", "data-aggregation": "تجمیع داده‌ها", "data-asset": "دارایی داده", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "URI توکن گوگل کلود.", "govern": "حکمرانی", "governance": "حکمرانی", + "grant-access": "Grant Access", + "granted": "Granted", "granularity": "دقت", "graph": "نمودار", "graph-settings": "تنظیمات نمودار", @@ -1255,6 +1262,7 @@ "many-to-one": "چند به یک", "march": "مارس", "mark-all-deleted-table-plural": "علامت‌گذاری همه جداول حذف شده", + "mark-as-granted": "Mark as Granted", "mark-deleted-entity": "علامت‌گذاری حذف {{entity}}", "mark-deleted-table-plural": "علامت‌گذاری جداول حذف شده", "markdown-guide": "راهنمای Markdown", @@ -1748,6 +1756,7 @@ "request-method": "روش درخواست", "request-schema-field": "فیلد اسکیما درخواست", "request-tag-plural": "برچسب‌های درخواست", + "requested-by": "Requested By", "required": "اجباری", "requirement-plural": "نیازمندی‌ها", "reset": "بازنشانی", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "حذف نرم", "soft-lowercase": "نرم", "sort": "مرتب سازی", + "sort-ascending": "Ascending", "sort-by-field": "مرتب‌سازی بر اساس {{field}}", + "sort-descending": "Descending", + "sort-order": "Sort Order", "source": "منبع", "source-aligned": "هم‌راستا با منبع", "source-column": "ستون منبع", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "تجربه صفحه موجودیت {{entity}} را برای شخصیت <0>{{persona}} شخصی‌سازی کنید", "customize-home-page-page-header-for-persona": "Personalize a experiência da página inicial para a <0>{{persona}} persona", "customize-your-navigation-subheader": "منوی ناوبری کناری را برای دسترسی بهتر مدیریت و سازماندهی کنید.", + "data-access-request-already-exists": "A pending data access request already exists for this asset.", + "data-access-request-awaiting-grant-message": "Your access request has been approved and is awaiting provisioning. Access will be available shortly once it has been granted.", "data-access-request-message": "درخواست دسترسی به داده برای", "data-asset-has-been-action-type": "دارایی داده‌ای {{actionType}} شده است", "data-asset-rules-message": "د ډیټا اثاثو قواعد ستاسو سره د پلیټ فارم په کچه د میټاډیټا اعتبار وړتیا اداره کولو کې مرسته کوي. که چیرې یو اثاثه د قانون پیروي ونکړي، نو تاسو به ونشئ کولی پدې کې کومې بدلونه رامینځته کړئ پرته له دې چې لومړی د اعتبار وړتیا سمه کړئ.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json index cfc97db89bab..bcfefbd6e0da 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json @@ -16,6 +16,7 @@ "access-policy-name": "Nome da Política de Acesso", "access-request-plural": "Solicitações de acesso", "access-token": "Token de Acesso", + "access-type": "Tipo de acesso", "accessed": "Acessado", "account": "Conta", "account-email": "E-mail da conta", @@ -154,6 +155,7 @@ "approve": "Aprovar", "approved": "Aprovado", "approved-entity": "Entidade aprovada {{entity}}", + "approver": "Aprovador", "approver-plural": "Aprovadores", "april": "Abril", "archived": "Arquivado", @@ -488,6 +490,9 @@ "dashboard-plural": "Painéis", "dashboard-service": "Serviço de Painel", "data": "Dados", + "data-access-request": "Solicitação de acesso a dados", + "data-access-request-awaiting-grant": "Aprovado – Aguardando concessão", + "data-access-request-plural": "Solicitações de acesso a dados", "data-aggregate": "Agregação de Dados", "data-aggregation": "Agregação de Dados", "data-asset": "Ativo de Dados", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "URI do token do Google Cloud.", "govern": "Governar", "governance": "Governança", + "grant-access": "Conceder acesso", + "granted": "Concedido", "granularity": "Granularidade", "graph": "Grafo", "graph-settings": "Configurações do Grafo", @@ -1255,6 +1262,7 @@ "many-to-one": "Muitos para um", "march": "Março", "mark-all-deleted-table-plural": "Marcar Todas as Tabelas Excluídas", + "mark-as-granted": "Marcar como concedido", "mark-deleted-entity": "Marcar {{entity}} Como Excluída", "mark-deleted-table-plural": "Marcar Tabelas Excluídas", "markdown-guide": "Guia Markdown", @@ -1748,6 +1756,7 @@ "request-method": "Método de solicitação", "request-schema-field": "Campo de esquema da requisição", "request-tag-plural": "Solicitar Tags", + "requested-by": "Solicitado por", "required": "Obrigatória", "requirement-plural": "Requisitos", "reset": "Redefinir", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "exclusão suave", "soft-lowercase": "suave", "sort": "Ordenar", + "sort-ascending": "Crescente", "sort-by-field": "Classificar por {{field}}", + "sort-descending": "Decrescente", + "sort-order": "Ordem de classificação", "source": "Fonte", "source-aligned": "Alinhado à Fonte", "source-column": "Coluna de Fonte", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "Personalize a experiência da Página de Entidade {{entity}} para a persona <0>{{persona}}", "customize-home-page-page-header-for-persona": "Personalize a experiência da página inicial para a <0>{{persona}} persona", "customize-your-navigation-subheader": "Gerencie e organize o menu de navegação lateral para melhorar a acessibilidade.", + "data-access-request-already-exists": "Já existe uma solicitação de acesso a dados pendente para este ativo.", + "data-access-request-awaiting-grant-message": "Sua solicitação de acesso foi aprovada e está aguardando provisionamento. O acesso estará disponível em breve assim que for concedido.", "data-access-request-message": "Solicitação de acesso a dados para", "data-asset-has-been-action-type": "O Ativo de Dados foi {{actionType}}", "data-asset-rules-message": "As Regras de Ativos de Dados ajudam você a gerenciar validações de metadados no nível da plataforma. Se um ativo não seguir uma regra, você não conseguirá fazer alterações nele a menos que corrija a validação primeiro.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json index e0cf0348f301..f1e574cc6ac1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json @@ -16,6 +16,7 @@ "access-policy-name": "Nome da Política de Acesso", "access-request-plural": "Pedidos de acesso", "access-token": "Token de Acesso", + "access-type": "Access Type", "accessed": "Acessado", "account": "Conta", "account-email": "E-mail da conta", @@ -154,6 +155,7 @@ "approve": "Aprovar", "approved": "Aprovado", "approved-entity": "Entidade aprovada {{entity}}", + "approver": "Approver", "approver-plural": "Aprovadores", "april": "Abril", "archived": "Arquivado", @@ -488,6 +490,9 @@ "dashboard-plural": "Painéis", "dashboard-service": "Serviço de Painel", "data": "Dados", + "data-access-request": "Data Access Request", + "data-access-request-awaiting-grant": "Approved – Awaiting Grant", + "data-access-request-plural": "Data Access Requests", "data-aggregate": "Agregação de Dados", "data-aggregation": "Agregação de Dados", "data-asset": "Ativo de Dados", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "URI do token do Google Cloud.", "govern": "Governar", "governance": "Governança", + "grant-access": "Grant Access", + "granted": "Granted", "granularity": "Granularidade", "graph": "Grafo", "graph-settings": "Definições do Grafo", @@ -1255,6 +1262,7 @@ "many-to-one": "Muitos para um", "march": "Março", "mark-all-deleted-table-plural": "Marcar Todas as Tabelas Excluídas", + "mark-as-granted": "Mark as Granted", "mark-deleted-entity": "Marcar {{entity}} Como Excluída", "mark-deleted-table-plural": "Marcar Tabelas Excluídas", "markdown-guide": "Guia Markdown", @@ -1748,6 +1756,7 @@ "request-method": "Método de Pedido", "request-schema-field": "Campo do Esquema de Pedido", "request-tag-plural": "Solicitar Etiquetas", + "requested-by": "Requested By", "required": "Obrigatório", "requirement-plural": "Requisitos", "reset": "Redefinir", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "soft delete", "soft-lowercase": "suave", "sort": "Ordenar", + "sort-ascending": "Ascending", "sort-by-field": "Ordenar por {{field}}", + "sort-descending": "Descending", + "sort-order": "Sort Order", "source": "Fonte", "source-aligned": "Alinhado à Fonte", "source-column": "Coluna de Fonte", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "Personalize a experiência da Página da Entidade {{entity}} para a persona <0>{{persona}}", "customize-home-page-page-header-for-persona": "Personalize a experiência da página inicial para a <0>{{persona}} persona", "customize-your-navigation-subheader": "Gerir e organizar o menu de navegação lateral para melhor acessibilidade.", + "data-access-request-already-exists": "A pending data access request already exists for this asset.", + "data-access-request-awaiting-grant-message": "Your access request has been approved and is awaiting provisioning. Access will be available shortly once it has been granted.", "data-access-request-message": "Pedido de acesso a dados para", "data-asset-has-been-action-type": "O Ativo de Dados foi {{actionType}}", "data-asset-rules-message": "As Regras de Activos de Dados ajudam-no a gerir validações de metadados ao nível da plataforma. Se um activo não seguir uma regra, não poderá fazer alterações ao mesmo a menos que corrija primeiro a validação.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json index e282c98500e9..1eb18423d043 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json @@ -16,6 +16,7 @@ "access-policy-name": "Имя Политики Доступа", "access-request-plural": "Запросы доступа", "access-token": "Токен доступа", + "access-type": "Тип доступа", "accessed": "Доступ выполнен", "account": "Аккаунт", "account-email": "Адрес электронной почты", @@ -154,6 +155,7 @@ "approve": "Подтвердить", "approved": "Подтверждено", "approved-entity": "Одобренная {{entity}}", + "approver": "Утверждающий", "approver-plural": "Утверждающие", "april": "Апрель", "archived": "В архиве", @@ -488,6 +490,9 @@ "dashboard-plural": "Дашборды", "dashboard-service": "Сервис информационной панели", "data": "Данные", + "data-access-request": "Запрос на доступ к данным", + "data-access-request-awaiting-grant": "Одобрено – ожидает предоставления", + "data-access-request-plural": "Запросы на доступ к данным", "data-aggregate": "Сводные данные", "data-aggregation": "Агрегация данных", "data-asset": "Объект данных", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "URI токена Google Cloud.", "govern": "Глоссарий", "governance": "Руководство", + "grant-access": "Предоставить доступ", + "granted": "Предоставлен", "granularity": "Гранулярность", "graph": "Граф", "graph-settings": "Настройки графа", @@ -1255,6 +1262,7 @@ "many-to-one": "Многие к одному", "march": "Март", "mark-all-deleted-table-plural": "Отметить все удаленные таблицы", + "mark-as-granted": "Отметить как предоставленный", "mark-deleted-entity": "Отметить объект «{{entity}}» как удаленный", "mark-deleted-table-plural": "Отметить удаленные таблицы", "markdown-guide": "Руководство по Markdown", @@ -1748,6 +1756,7 @@ "request-method": "Метод запроса", "request-schema-field": "Поле схемы запроса", "request-tag-plural": "Предложить тег", + "requested-by": "Запрошено", "required": "Необходимый", "requirement-plural": "Требования", "reset": "Сбросить", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "удалил(а)", "soft-lowercase": "мягко", "sort": "Сортировка", + "sort-ascending": "По возрастанию", "sort-by-field": "Сортировать по {{field}}", + "sort-descending": "По убыванию", + "sort-order": "Порядок сортировки", "source": "Источник", "source-aligned": "Ориентированный на источник", "source-column": "Источник столбца", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "Персонализируйте страницу объекта «{{entity}}» для персоны <0>{{persona}}", "customize-home-page-page-header-for-persona": "Персонализируйте опыт домашней страницы для <0>{{persona}} персоны", "customize-your-navigation-subheader": "Управляйте и организуйте боковое меню навигации для лучшей доступности.", + "data-access-request-already-exists": "Для этого ресурса уже существует ожидающий запрос на доступ к данным.", + "data-access-request-awaiting-grant-message": "Ваш запрос на доступ одобрен и ожидает предоставления. Доступ будет доступен в ближайшее время после его предоставления.", "data-access-request-message": "Запрос доступа к данным для", "data-asset-has-been-action-type": "Объект данных был {{actionType}}", "data-asset-rules-message": "Правила ресурсов данных помогают вам управлять валидацией метаданных на уровне платформы. Если ресурс не следует правилу, вы не сможете вносить в него изменения, пока не исправите валидацию сначала.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json index 599ef98b6505..aee951433c5e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json @@ -16,6 +16,7 @@ "access-policy-name": "ชื่อนโยบายการเข้าถึง", "access-request-plural": "คำขอเข้าถึง", "access-token": "โทเค็นการเข้าถึง", + "access-type": "Access Type", "accessed": "เข้าใช้", "account": "บัญชี", "account-email": "อีเมลบัญชี", @@ -154,6 +155,7 @@ "approve": "อนุมัติ", "approved": "ได้รับการอนุมัติ", "approved-entity": "{{entity}} ที่ได้รับการอนุมัติ", + "approver": "Approver", "approver-plural": "ผู้อนุมัติ", "april": "เมษายน", "archived": "เก็บถาวร", @@ -488,6 +490,9 @@ "dashboard-plural": "แดชบอร์ดหลายรายการ", "dashboard-service": "บริการแดชบอร์ด", "data": "ข้อมูล", + "data-access-request": "Data Access Request", + "data-access-request-awaiting-grant": "Approved – Awaiting Grant", + "data-access-request-plural": "Data Access Requests", "data-aggregate": "การรวมข้อมูล", "data-aggregation": "การรวบรวมข้อมูล", "data-asset": "ทรัพย์สินข้อมูล", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "URI โทเค็น Google Cloud", "govern": "กำกับดูแล", "governance": "การกำกับดูแล", + "grant-access": "Grant Access", + "granted": "Granted", "granularity": "ระดับความละเอียด", "graph": "กราฟ", "graph-settings": "ตั้งค่ากราฟ", @@ -1255,6 +1262,7 @@ "many-to-one": "หลายต่อหนึ่ง", "march": "มีนาคม", "mark-all-deleted-table-plural": "ทำเครื่องหมายตารางที่ถูกลบทั้งหมด", + "mark-as-granted": "Mark as Granted", "mark-deleted-entity": "ทำเครื่องหมายเอนทิตีที่ถูกลบ {{entity}}", "mark-deleted-table-plural": "ทำเครื่องหมายตารางที่ถูกลบ", "markdown-guide": "คู่มือ Markdown", @@ -1748,6 +1756,7 @@ "request-method": "วิธีการของคำขอ", "request-schema-field": "ฟิลด์สคีมาของคำขอ", "request-tag-plural": "แท็กคำขอ", + "requested-by": "Requested By", "required": "จำเป็น", "requirement-plural": "ข้อกำหนด", "reset": "รีเซ็ต", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "ลบแบบนุ่มนวล", "soft-lowercase": "นุ่มนวล", "sort": "จัดเรียง", + "sort-ascending": "Ascending", "sort-by-field": "เรียงตาม {{field}}", + "sort-descending": "Descending", + "sort-order": "Sort Order", "source": "แหล่งที่มา", "source-aligned": "ตรงตามแหล่งที่มา", "source-column": "คอลัมน์แหล่งที่มา", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "ปรับแต่งประสบการณ์หน้าเอนทิตี {{entity}} สำหรับบุคคล <0>{{persona}}", "customize-home-page-page-header-for-persona": "ปรับแต่งประสบการณ์หน้าแรกสำหรับ <0>{{persona}} เพอร์โซนา", "customize-your-navigation-subheader": "จัดการและจัดระเบียบเมนูนำทางด้านข้างเพื่อการเข้าถึงที่ดีขึ้น", + "data-access-request-already-exists": "A pending data access request already exists for this asset.", + "data-access-request-awaiting-grant-message": "Your access request has been approved and is awaiting provisioning. Access will be available shortly once it has been granted.", "data-access-request-message": "คำขอเข้าถึงข้อมูลสำหรับ", "data-asset-has-been-action-type": "สินทรัพย์ข้อมูลได้ทำการ {{actionType}}", "data-asset-rules-message": "กฎของทรัพย์สินข้อมูลช่วยคุณจัดการการตรวจสอบความถูกต้องของเมตาดาต้าในระดับแพลตฟอร์ม หากทรัพย์สินไม่ปฏิบัติตามกฎ คุณจะไม่สามารถทำการเปลี่ยนแปลงใดๆ ได้จนกว่าคุณจะแก้ไขการตรวจสอบก่อน", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json index e30bdd5ed556..d0f845027fbb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json @@ -16,6 +16,7 @@ "access-policy-name": "Erişim Politikası Adı", "access-request-plural": "Erişim Talepleri", "access-token": "Erişim Anahtarı", + "access-type": "Access Type", "accessed": "Erişildi", "account": "Hesap", "account-email": "Hesap e-postası", @@ -154,6 +155,7 @@ "approve": "Onayla", "approved": "Onaylandı", "approved-entity": "Onaylanan {{entity}}", + "approver": "Approver", "approver-plural": "Onaylayıcılar", "april": "Nisan", "archived": "Arşivlendi", @@ -488,6 +490,9 @@ "dashboard-plural": "Kontrol Panelleri", "dashboard-service": "Kontrol Paneli Servisi", "data": "Veri", + "data-access-request": "Data Access Request", + "data-access-request-awaiting-grant": "Approved – Awaiting Grant", + "data-access-request-plural": "Data Access Requests", "data-aggregate": "Veri Toplamı", "data-aggregation": "Veri Toplama", "data-asset": "Veri Varlığı", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "Google Cloud anahtar uri'si.", "govern": "Yönet", "governance": "Yönetişim", + "grant-access": "Grant Access", + "granted": "Granted", "granularity": "Taneciklilik", "graph": "Grafik", "graph-settings": "Grafik Ayarları", @@ -1255,6 +1262,7 @@ "many-to-one": "Çoka Bir", "march": "Mart", "mark-all-deleted-table-plural": "Tüm Silinmiş Tabloları İşaretle", + "mark-as-granted": "Mark as Granted", "mark-deleted-entity": "Silinmiş {{entity}} İşaretle", "mark-deleted-table-plural": "Silinmiş Tabloları İşaretle", "markdown-guide": "Markdown Kılavuzu", @@ -1748,6 +1756,7 @@ "request-method": "İstek Metodu", "request-schema-field": "İstek Şema Alanı", "request-tag-plural": "Etiket İste", + "requested-by": "Requested By", "required": "Gerekli", "requirement-plural": "Gereksinimler", "reset": "Sıfırla", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "geçici silindi", "soft-lowercase": "geçici", "sort": "Sırala", + "sort-ascending": "Ascending", "sort-by-field": "{{field}} alanına göre sırala", + "sort-descending": "Descending", + "sort-order": "Sort Order", "source": "Kaynak", "source-aligned": "Kaynak Odaklı", "source-column": "Kaynak Sütun", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "<0>{{persona}} personası için {{entity}} Varlık Sayfası deneyimini kişiselleştirin", "customize-home-page-page-header-for-persona": "<0>{{persona}} kişiliği için Ana Sayfa deneyimini kişiselleştirin", "customize-your-navigation-subheader": "Daha iyi erişilebilirlik için yan gezinme menüsünü yönetin ve düzenleyin.", + "data-access-request-already-exists": "A pending data access request already exists for this asset.", + "data-access-request-awaiting-grant-message": "Your access request has been approved and is awaiting provisioning. Access will be available shortly once it has been granted.", "data-access-request-message": "Veri erişim talebi:", "data-asset-has-been-action-type": "Veri Varlığı {{actionType}} oldu", "data-asset-rules-message": "Veri Varlığı Kuralları, platform düzeyinde metadata doğrulamalarını yönetmenize yardımcı olur. Bir varlık bir kuralı takip etmezse, önce doğrulamayı düzeltmediğiniz sürece herhangi bir değişiklik yapamazsınız.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json index 484bd3045974..d8353e54e392 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json @@ -16,6 +16,7 @@ "access-policy-name": "访问策略名称", "access-request-plural": "访问请求", "access-token": "访问令牌", + "access-type": "访问类型", "accessed": "已访问", "account": "帐号", "account-email": "帐号邮箱", @@ -154,6 +155,7 @@ "approve": "批准", "approved": "已批准", "approved-entity": "已批准的 {{entity}}", + "approver": "审批人", "approver-plural": "审批人", "april": "四月", "archived": "已归档", @@ -488,6 +490,9 @@ "dashboard-plural": "仪表板", "dashboard-service": "仪表盘服务", "data": "数据", + "data-access-request": "数据访问请求", + "data-access-request-awaiting-grant": "已批准 – 等待授权", + "data-access-request-plural": "数据访问请求", "data-aggregate": "数据聚合", "data-aggregation": "数据聚合", "data-asset": "数据资产", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "Google Cloud 令牌 URI", "govern": "治理", "governance": "数据治理", + "grant-access": "授予访问权限", + "granted": "已授权", "granularity": "粒度", "graph": "图", "graph-settings": "图设置", @@ -1255,6 +1262,7 @@ "many-to-one": "多对一", "march": "三月", "mark-all-deleted-table-plural": "标记所有已删除的表", + "mark-as-granted": "标记为已授权", "mark-deleted-entity": "标记已删除的{{entity}}", "mark-deleted-table-plural": "标记已删除的表", "markdown-guide": "Markdown 指南", @@ -1748,6 +1756,7 @@ "request-method": "请求方式", "request-schema-field": "请求架构字段", "request-tag-plural": "请求补充标签", + "requested-by": "请求者", "required": "必需的", "requirement-plural": "需求", "reset": "重置", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "已软删除", "soft-lowercase": "软", "sort": "排序", + "sort-ascending": "升序", "sort-by-field": "按 {{field}} 排序", + "sort-descending": "降序", + "sort-order": "排序顺序", "source": "源", "source-aligned": "源对齐", "source-column": "源列", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "为<0>{{persona}}角色个性化{{entity}}实体页面体验", "customize-home-page-page-header-for-persona": "为 <0>{{persona}} 角色个性化首页体验", "customize-your-navigation-subheader": "管理和组织侧边导航菜单以提高可访问性。", + "data-access-request-already-exists": "该资产已存在待处理的数据访问请求。", + "data-access-request-awaiting-grant-message": "您的访问请求已获批准,正在等待配置。一旦授权,访问权限将很快可用。", "data-access-request-message": "数据访问请求:", "data-asset-has-been-action-type": "数据资产已{{actionType}}", "data-asset-rules-message": "数据资产规则帮助您在平台级别管理元数据验证。如果资产不遵循规则,您将无法对其进行任何更改,除非您首先修复验证问题。", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json index f1edf991fdcf..b097c85e93a5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json @@ -16,6 +16,7 @@ "access-policy-name": "存取政策名稱", "access-request-plural": "存取請求", "access-token": "存取權杖", + "access-type": "Access Type", "accessed": "已存取", "account": "帳戶", "account-email": "帳戶電子郵件", @@ -154,6 +155,7 @@ "approve": "核准", "approved": "已核准", "approved-entity": "已批准的 {{entity}}", + "approver": "Approver", "approver-plural": "审批人", "april": "四月", "archived": "已封存", @@ -488,6 +490,9 @@ "dashboard-plural": "儀表板", "dashboard-service": "儀表板服務", "data": "資料", + "data-access-request": "Data Access Request", + "data-access-request-awaiting-grant": "Approved – Awaiting Grant", + "data-access-request-plural": "Data Access Requests", "data-aggregate": "資料彙總", "data-aggregation": "資料彙總", "data-asset": "資料資產", @@ -993,6 +998,8 @@ "google-cloud-token-uri": "Google Cloud 權杖 URI。", "govern": "治理", "governance": "治理", + "grant-access": "Grant Access", + "granted": "Granted", "granularity": "粒度", "graph": "圖表", "graph-settings": "圖表設定", @@ -1255,6 +1262,7 @@ "many-to-one": "多對一", "march": "三月", "mark-all-deleted-table-plural": "標記所有已刪除的資料表", + "mark-as-granted": "Mark as Granted", "mark-deleted-entity": "標記已刪除的 {{entity}}", "mark-deleted-table-plural": "標記已刪除的資料表", "markdown-guide": "Markdown 指南", @@ -1748,6 +1756,7 @@ "request-method": "請求方法", "request-schema-field": "請求結構欄位", "request-tag-plural": "請求標籤", + "requested-by": "Requested By", "required": "必填", "requirement-plural": "需求", "reset": "重設", @@ -1988,7 +1997,10 @@ "soft-deleted-lowercase": "軟刪除", "soft-lowercase": "軟", "sort": "排序", + "sort-ascending": "Ascending", "sort-by-field": "依 {{field}} 排序", + "sort-descending": "Descending", + "sort-order": "Sort Order", "source": "來源", "source-aligned": "來源導向", "source-column": "來源欄位", @@ -2571,6 +2583,8 @@ "customize-entity-landing-page-header-for-persona": "為 <0>{{persona}} 角色個人化 {{entity}} 實體頁面體驗", "customize-home-page-page-header-for-persona": "為 <0>{{persona}} 角色個人化首頁體驗", "customize-your-navigation-subheader": "管理和組織側邊導覽選單,以提高可及性。", + "data-access-request-already-exists": "A pending data access request already exists for this asset.", + "data-access-request-awaiting-grant-message": "Your access request has been approved and is awaiting provisioning. Access will be available shortly once it has been granted.", "data-access-request-message": "資料存取請求:", "data-asset-has-been-action-type": "資料資產已被 {{actionType}}", "data-asset-rules-message": "資料資產規則幫助您在平台層級管理元資料驗證。如果資產不遵循規則,除非您先修復驗證,否則您將無法對其進行任何更改。", diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/tasksAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/tasksAPI.ts index 180abf319823..c316ebaa8bf3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/tasksAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/tasksAPI.ts @@ -13,205 +13,50 @@ import { AxiosResponse } from 'axios'; import { Operation } from 'fast-json-patch'; import { PagingResponse } from 'Models'; -import { EntityReference } from '../generated/entity/data/table'; +import { + CreateTask, + TaskCategory, + TaskPriority, + TaskType, +} from '../generated/api/tasks/createTask'; +import { ResolveTask } from '../generated/api/tasks/resolveTask'; +import { TaskCount } from '../generated/api/tasks/taskCount'; +import { Task, TaskStatus } from '../generated/entity/tasks/task'; import { Include } from '../generated/type/include'; -import { TagLabel } from '../generated/type/tagLabel'; import APIClient from './index'; -// Task status enum - matches backend TaskEntityStatus -export enum TaskEntityStatus { - Open = 'Open', - InProgress = 'InProgress', - Pending = 'Pending', - Approved = 'Approved', - Rejected = 'Rejected', - Completed = 'Completed', - Cancelled = 'Cancelled', - Failed = 'Failed', - Revoked = 'Revoked', +export { + TaskCategory, + TaskPriority, + TaskType as TaskEntityType, +} from '../generated/api/tasks/createTask'; +export { ResolutionType as TaskResolutionType } from '../generated/api/tasks/resolveTask'; +export { TaskStatus as TaskEntityStatus } from '../generated/entity/tasks/task'; +export type { TaskComment } from '../generated/entity/tasks/task'; +export type { GenericTaskPayload as TaskPayload } from '../generated/type/genericTaskPayload'; + +// Data access type enum - matches backend DataAccessType +export enum DataAccessType { + FullAccess = 'FullAccess', + ColumnLevel = 'ColumnLevel', + Masked = 'Masked', } -// Task category enum - matches backend TaskCategory -export enum TaskCategory { - Approval = 'Approval', - DataAccess = 'DataAccess', - MetadataUpdate = 'MetadataUpdate', - Incident = 'Incident', - Review = 'Review', - Custom = 'Custom', -} - -// Task type enum - matches backend TaskEntityType -export enum TaskEntityType { - GlossaryApproval = 'GlossaryApproval', - RequestApproval = 'RequestApproval', - DataAccessRequest = 'DataAccessRequest', - DescriptionUpdate = 'DescriptionUpdate', - TagUpdate = 'TagUpdate', - OwnershipUpdate = 'OwnershipUpdate', - TierUpdate = 'TierUpdate', - DomainUpdate = 'DomainUpdate', - Suggestion = 'Suggestion', - TestCaseResolution = 'TestCaseResolution', - IncidentResolution = 'IncidentResolution', - PipelineReview = 'PipelineReview', - DataQualityReview = 'DataQualityReview', - CustomTask = 'CustomTask', -} - -// Task priority enum - matches backend TaskPriority -export enum TaskPriority { - Critical = 'Critical', - High = 'High', - Medium = 'Medium', - Low = 'Low', -} - -// Task resolution type enum -export enum TaskResolutionType { - Approved = 'Approved', - Rejected = 'Rejected', - Completed = 'Completed', - Cancelled = 'Cancelled', - TimedOut = 'TimedOut', - AutoApproved = 'AutoApproved', - AutoRejected = 'AutoRejected', -} - -// Task comment interface -export interface TaskComment { - id: string; - message: string; - author: EntityReference; - createdAt: number; - reactions?: unknown[]; -} - -// Task resolution interface -export interface TaskResolution { - type: TaskResolutionType; - resolvedBy?: EntityReference; - resolvedAt?: number; - comment?: string; - newValue?: string; -} - -export interface TaskAvailableTransition { - id: string; - label: string; - targetStageId: string; - targetTaskStatus: TaskEntityStatus; - resolutionType?: TaskResolutionType; - formRef?: string; - requiresComment?: boolean; -} - -// Task payload interface - union of all payload types -export interface TaskPayload { - [key: string]: unknown; - // SuggestionPayload fields - suggestionType?: string; - suggestedValue?: string; - currentValue?: string; - confidence?: number; - source?: string; - reasoning?: string; - // DescriptionUpdatePayload fields - newDescription?: string; - currentDescription?: string; - // Common - fieldPath?: string; - field?: string; - // TagUpdatePayload fields - currentTags?: TagLabel[]; - tagsToAdd?: TagLabel[]; - tagsToRemove?: TagLabel[]; - operation?: string; -} - -// Task entity interface - matches backend Task entity -export interface Task { - id: string; - taskId: string; - name: string; - displayName?: string; - fullyQualifiedName?: string; - description?: string; - category: TaskCategory; - type: TaskEntityType; - status: TaskEntityStatus; - priority: TaskPriority; - about?: EntityReference; - domain?: EntityReference; - domains?: EntityReference[]; - createdBy?: EntityReference; - assignees?: EntityReference[]; - reviewers?: EntityReference[]; - watchers?: EntityReference[]; - payload?: TaskPayload; - resolution?: TaskResolution; - dueDate?: number; - externalReference?: { - system: string; - externalId: string; - externalUrl?: string; - syncStatus?: string; - lastSyncedAt?: number; - }; - workflowInstanceId?: string; - workflowDefinitionId?: string; - workflowStageId?: string; - workflowStageDisplayName?: string; - availableTransitions?: TaskAvailableTransition[]; - taskFormSchemaId?: string; - taskFormSchemaVersion?: number; - comments?: TaskComment[]; - commentCount?: number; - tags?: TagLabel[]; - createdAt?: number; - version?: number; - updatedAt?: number; - updatedBy?: string; - href?: string; - changeDescription?: unknown; - deleted?: boolean; -} - -// CreateTask request interface -export interface CreateTask { - name: string; - displayName?: string; - description?: string; - category: TaskCategory; - type: TaskEntityType; - priority?: TaskPriority; - about?: string; // Entity link of the asset, format: <#E::{entityType}::{fqn}> - domain?: string; - assignees?: string[]; // FQNs of users or teams - reviewers?: string[]; // FQNs of users or teams - payload?: TaskPayload; - dueDate?: number; - externalReference?: { - system: string; - externalId: string; - externalUrl?: string; - }; - tags?: TagLabel[]; -} - -// ResolveTask request interface -export interface ResolveTask { - transitionId?: string; - resolutionType?: TaskResolutionType; - comment?: string; - newValue?: string; - payload?: TaskPayload; +export enum DarWorkflowStage { + Review = 'review', + Approved = 'approved', + Granted = 'granted', } const BASE_URL = '/tasks'; +// 'Active' is a superset of 'Open' (Open/InProgress/Pending) that also includes +// Approved and Granted; used by the DAR hook so awaiting-grant and active-access +// requests are surfaced. 'Closed' keeps the legacy semantics that include +// Approved for non-DAR workflows where it is terminal. export enum TaskStatusGroup { Open = 'open', + Active = 'active', Closed = 'closed', } export type TaskCountView = @@ -225,7 +70,7 @@ export type TaskCountView = interface TaskScopedListParams { fields?: string; - status?: TaskEntityStatus; + status?: TaskStatus; statusGroup?: TaskStatusGroup; domain?: string; limit?: number; @@ -236,15 +81,19 @@ interface TaskScopedListParams { export interface ListTasksParams { fields?: string; - status?: TaskEntityStatus; + status?: TaskStatus; statusGroup?: TaskStatusGroup; category?: TaskCategory; - type?: TaskEntityType; + type?: TaskType; domain?: string; priority?: TaskPriority; assignee?: string; createdBy?: string; + createdById?: string; aboutEntity?: string; + aboutService?: string; + approver?: string; + approverId?: string; mentionedUser?: string; limit?: number; before?: string; @@ -252,6 +101,24 @@ export interface ListTasksParams { include?: Include; } +export interface ListDataAccessRequestsParams { + fields?: string; + status?: TaskStatus; + statusGroup?: TaskStatusGroup; + dataset?: string; + service?: string; + requestedBy?: string; + requestedById?: string; + approver?: string; + approverId?: string; + accessType?: DataAccessType; + domain?: string; + sortOrder?: 'asc' | 'desc'; + limit?: number; + offset?: number; + include?: Include; +} + /** * Get a list of tasks with optional filters. */ @@ -263,19 +130,24 @@ export const listTasks = async (params?: ListTasksParams) => { return response.data; }; +/** + * List Data Access Requests with DAR-specific filters and offset-based pagination. + */ +export const listDataAccessRequests = async ( + params?: ListDataAccessRequestsParams +) => { + const response = await APIClient.get>( + `${BASE_URL}/dataAccessRequests`, + { params } + ); + + return response.data; +}; + /** * Get tasks assigned to the current user or their teams. */ -export const listMyAssignedTasks = async (params?: { - fields?: string; - status?: TaskEntityStatus; - statusGroup?: TaskStatusGroup; - domain?: string; - limit?: number; - before?: string; - after?: string; - include?: Include; -}) => { +export const listMyAssignedTasks = async (params?: TaskScopedListParams) => { const response = await APIClient.get>( `${BASE_URL}/assigned`, { params } @@ -497,13 +369,10 @@ export const getTaskCounts = async (params?: { mentionedUser?: string; view?: TaskCountView; domain?: string; -}) => { - const response = await APIClient.get<{ - open: number; - inProgress: number; - completed: number; - total: number; - }>(`${BASE_URL}/count`, { params }); +}): Promise => { + const response = await APIClient.get(`${BASE_URL}/count`, { + params, + }); return response.data; };