diff --git a/bootstrap/sql/migrations/native/1.12.0/mysql/schemaChanges.sql b/bootstrap/sql/migrations/native/1.12.0/mysql/schemaChanges.sql
index 17d2dd0e7d38..db5f93e44c97 100644
--- a/bootstrap/sql/migrations/native/1.12.0/mysql/schemaChanges.sql
+++ b/bootstrap/sql/migrations/native/1.12.0/mysql/schemaChanges.sql
@@ -51,6 +51,20 @@ UPDATE test_definition
SET json = JSON_SET(json, '$.enabled', true)
WHERE json_extract(json, '$.enabled') IS NULL;
+
+-- Create Learning Resource Entity Table
+CREATE TABLE IF NOT EXISTS learning_resource_entity (
+ id varchar(36) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,'$.id'))) STORED NOT NULL,
+ name varchar(3072) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,'$.fullyQualifiedName'))) VIRTUAL,
+ fqnHash varchar(256) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
+ json json NOT NULL,
+ updatedAt bigint UNSIGNED GENERATED ALWAYS AS (json_unquote(json_extract(`json`,'$.updatedAt'))) VIRTUAL NOT NULL,
+ updatedBy varchar(256) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,'$.updatedBy'))) VIRTUAL NOT NULL,
+ deleted TINYINT(1) GENERATED ALWAYS AS (IF(json_extract(json,'$.deleted') = TRUE, 1, 0)) VIRTUAL,
+ PRIMARY KEY (id),
+ UNIQUE KEY fqnHash (fqnHash)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+
-- Add updatedAt generated column to entity_extension table for efficient timestamp-based queries
-- This supports the listEntityHistoryByTimestamp API endpoint for retrieving entity versions within a time range
ALTER TABLE entity_extension
@@ -97,3 +111,4 @@ CREATE INDEX idx_test_suite_updated_at_id ON test_suite(updatedAt DESC, id DESC)
CREATE INDEX idx_test_case_updated_at_id ON test_case(updatedAt DESC, id DESC);
CREATE INDEX idx_api_collection_entity_updated_at_id ON api_collection_entity(updatedAt DESC, id DESC);
CREATE INDEX idx_api_endpoint_entity_updated_at_id ON api_endpoint_entity(updatedAt DESC, id DESC);
+>>>>>>> upstream/main
diff --git a/bootstrap/sql/migrations/native/1.12.0/postgres/schemaChanges.sql b/bootstrap/sql/migrations/native/1.12.0/postgres/schemaChanges.sql
index bdec1513b29d..ab7fa34a15c5 100644
--- a/bootstrap/sql/migrations/native/1.12.0/postgres/schemaChanges.sql
+++ b/bootstrap/sql/migrations/native/1.12.0/postgres/schemaChanges.sql
@@ -67,6 +67,21 @@ ON audit_log_event (service_name, event_ts DESC);
CREATE INDEX IF NOT EXISTS idx_audit_log_created_at
ON audit_log_event (created_at);
+
+
+-- Create Learning Resource Entity Table
+CREATE TABLE IF NOT EXISTS learning_resource_entity (
+ id character varying(36) GENERATED ALWAYS AS ((json ->> 'id'::text)) STORED NOT NULL,
+ name character varying(3072) GENERATED ALWAYS AS ((json ->> 'fullyQualifiedName'::text)) STORED,
+ fqnhash character varying(256) NOT NULL,
+ json jsonb NOT NULL,
+ updatedat bigint GENERATED ALWAYS AS (((json ->> 'updatedAt'::text))::bigint) STORED NOT NULL,
+ updatedby character varying(256) GENERATED ALWAYS AS ((json ->> 'updatedBy'::text)) STORED NOT NULL,
+ deleted BOOLEAN GENERATED ALWAYS AS ((json ->> 'deleted')::boolean) STORED,
+ PRIMARY KEY (id),
+ UNIQUE (fqnhash)
+);
+
-- Add updatedAt generated column to entity_extension table for efficient timestamp-based queries
-- This supports the listEntityHistoryByTimestamp API endpoint for retrieving entity versions within a time range
ALTER TABLE entity_extension
@@ -113,3 +128,4 @@ CREATE INDEX IF NOT EXISTS idx_test_suite_updated_at_id ON test_suite(updatedAt
CREATE INDEX IF NOT EXISTS idx_test_case_updated_at_id ON test_case(updatedAt DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_api_collection_entity_updated_at_id ON api_collection_entity(updatedAt DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_api_endpoint_entity_updated_at_id ON api_endpoint_entity(updatedAt DESC, id DESC);
+>>>>>>> upstream/main
diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/LearningResourceIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/LearningResourceIT.java
new file mode 100644
index 000000000000..9191a0dfe0b9
--- /dev/null
+++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/LearningResourceIT.java
@@ -0,0 +1,679 @@
+package org.openmetadata.it.tests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
+import org.openmetadata.it.util.SdkClients;
+import org.openmetadata.it.util.TestNamespace;
+import org.openmetadata.schema.api.learning.CreateLearningResource;
+import org.openmetadata.schema.api.learning.ResourceCategory;
+import org.openmetadata.schema.entity.learning.LearningResource;
+import org.openmetadata.schema.entity.learning.LearningResourceContext;
+import org.openmetadata.schema.entity.learning.LearningResourceSource;
+import org.openmetadata.schema.type.EntityHistory;
+import org.openmetadata.sdk.models.ListParams;
+import org.openmetadata.sdk.models.ListResponse;
+import org.openmetadata.sdk.services.learning.LearningResourceService;
+
+/**
+ * Integration tests for LearningResource entity operations.
+ *
+ *
Tests LearningResource CRUD operations, category validation, context handling, and
+ * learning-resource-specific validations.
+ *
+ *
Migrated from: org.openmetadata.service.resources.learning.LearningResourceResourceTest
+ */
+@Execution(ExecutionMode.CONCURRENT)
+public class LearningResourceIT extends BaseEntityIT {
+
+ public LearningResourceIT() {
+ supportsPatch = true;
+ supportsFollowers = false;
+ supportsTags = true;
+ supportsDataProducts = false;
+ supportsCustomExtension = true;
+ supportsSearchIndex = false;
+ supportsDomains = false;
+ }
+
+ // ===================================================================
+ // ABSTRACT METHOD IMPLEMENTATIONS (Required by BaseEntityIT)
+ // ===================================================================
+
+ @Override
+ protected CreateLearningResource createMinimalRequest(TestNamespace ns) {
+ return new CreateLearningResource()
+ .withName(ns.prefix("learning-resource"))
+ .withDescription("Test learning resource")
+ .withResourceType(CreateLearningResource.ResourceType.VIDEO)
+ .withCategories(List.of(ResourceCategory.DISCOVERY))
+ .withSource(
+ new LearningResourceSource()
+ .withProvider("YouTube")
+ .withUrl(URI.create("https://youtube.com/watch?v=test")))
+ .withContexts(List.of(new LearningResourceContext().withPageId("explore")));
+ }
+
+ @Override
+ protected CreateLearningResource createRequest(String name, TestNamespace ns) {
+ return new CreateLearningResource()
+ .withName(name)
+ .withDescription("Test learning resource")
+ .withResourceType(CreateLearningResource.ResourceType.ARTICLE)
+ .withCategories(List.of(ResourceCategory.DATA_GOVERNANCE))
+ .withSource(new LearningResourceSource().withUrl(URI.create("https://example.com/article")))
+ .withContexts(List.of(new LearningResourceContext().withPageId("glossary")));
+ }
+
+ @Override
+ protected LearningResource createEntity(CreateLearningResource createRequest) {
+ return getLearningResourceService().create(createRequest);
+ }
+
+ @Override
+ protected LearningResource getEntity(String id) {
+ return getLearningResourceService().get(id);
+ }
+
+ @Override
+ protected LearningResource getEntityByName(String fqn) {
+ return getLearningResourceService().getByName(fqn);
+ }
+
+ @Override
+ protected LearningResource patchEntity(String id, LearningResource entity) {
+ return getLearningResourceService().update(id, entity);
+ }
+
+ @Override
+ protected void deleteEntity(String id) {
+ getLearningResourceService().delete(id);
+ }
+
+ @Override
+ protected void restoreEntity(String id) {
+ getLearningResourceService().restore(id);
+ }
+
+ @Override
+ protected void hardDeleteEntity(String id) {
+ Map params = new HashMap<>();
+ params.put("hardDelete", "true");
+ getLearningResourceService().delete(id, params);
+ }
+
+ @Override
+ protected String getEntityType() {
+ return "learningResource";
+ }
+
+ @Override
+ protected void validateCreatedEntity(
+ LearningResource entity, CreateLearningResource createRequest) {
+ assertEquals(createRequest.getName(), entity.getName());
+
+ if (createRequest.getDescription() != null) {
+ assertEquals(createRequest.getDescription(), entity.getDescription());
+ }
+
+ if (createRequest.getDisplayName() != null) {
+ assertEquals(createRequest.getDisplayName(), entity.getDisplayName());
+ }
+
+ assertEquals(createRequest.getResourceType().value(), entity.getResourceType().value());
+ assertNotNull(entity.getCategories());
+ assertFalse(entity.getCategories().isEmpty());
+ assertNotNull(entity.getContexts());
+ assertFalse(entity.getContexts().isEmpty());
+
+ assertTrue(
+ entity.getFullyQualifiedName().contains(entity.getName()),
+ "FQN should contain resource name");
+ }
+
+ @Override
+ protected ListResponse listEntities(ListParams params) {
+ return getLearningResourceService().list(params);
+ }
+
+ @Override
+ protected LearningResource getEntityWithFields(String id, String fields) {
+ return getLearningResourceService().get(id, fields);
+ }
+
+ @Override
+ protected LearningResource getEntityByNameWithFields(String fqn, String fields) {
+ return getLearningResourceService().getByName(fqn, fields);
+ }
+
+ @Override
+ protected LearningResource getEntityIncludeDeleted(String id) {
+ return getLearningResourceService().get(id, null, "deleted");
+ }
+
+ @Override
+ protected EntityHistory getVersionHistory(UUID id) {
+ return getLearningResourceService().getVersionList(id);
+ }
+
+ @Override
+ protected LearningResource getVersion(UUID id, Double version) {
+ return getLearningResourceService().getVersion(id.toString(), version);
+ }
+
+ // ===================================================================
+ // AI CATEGORY TESTS
+ // ===================================================================
+
+ @Test
+ void post_learningResourceWithAICategory_200_OK(TestNamespace ns) {
+ CreateLearningResource request =
+ new CreateLearningResource()
+ .withName(ns.prefix("ai-resource"))
+ .withDescription("AI tutorial")
+ .withResourceType(CreateLearningResource.ResourceType.VIDEO)
+ .withCategories(List.of(ResourceCategory.AI))
+ .withSource(
+ new LearningResourceSource()
+ .withProvider("YouTube")
+ .withUrl(URI.create("https://youtube.com/watch?v=ai-tutorial")))
+ .withContexts(List.of(new LearningResourceContext().withPageId("askCollate")));
+
+ LearningResource resource = createEntity(request);
+ assertNotNull(resource.getId());
+ assertEquals(1, resource.getCategories().size());
+ assertTrue(resource.getCategories().contains(ResourceCategory.AI));
+ }
+
+ @Test
+ void post_learningResourceWithAIAndOtherCategories_200_OK(TestNamespace ns) {
+ CreateLearningResource request =
+ new CreateLearningResource()
+ .withName(ns.prefix("ai-discovery-resource"))
+ .withDescription("AI and Discovery tutorial")
+ .withResourceType(CreateLearningResource.ResourceType.STORYLANE)
+ .withCategories(List.of(ResourceCategory.AI, ResourceCategory.DISCOVERY))
+ .withSource(
+ new LearningResourceSource()
+ .withProvider("Storylane")
+ .withUrl(URI.create("https://storylane.app/embed/ai-discovery")))
+ .withContexts(
+ List.of(
+ new LearningResourceContext().withPageId("askCollate"),
+ new LearningResourceContext().withPageId("explore")));
+
+ LearningResource resource = createEntity(request);
+ assertEquals(2, resource.getCategories().size());
+ assertTrue(resource.getCategories().contains(ResourceCategory.AI));
+ assertTrue(resource.getCategories().contains(ResourceCategory.DISCOVERY));
+ }
+
+ // ===================================================================
+ // ALL CATEGORIES TESTS
+ // ===================================================================
+
+ @Test
+ void post_learningResourceWithAllCategories_200_OK(TestNamespace ns) {
+ for (ResourceCategory category : ResourceCategory.values()) {
+ CreateLearningResource request =
+ new CreateLearningResource()
+ .withName(ns.prefix("cat-" + category.value().toLowerCase()))
+ .withDescription("Resource for " + category.value())
+ .withResourceType(CreateLearningResource.ResourceType.ARTICLE)
+ .withCategories(List.of(category))
+ .withSource(
+ new LearningResourceSource()
+ .withUrl(URI.create("https://example.com/" + category.value())))
+ .withContexts(List.of(new LearningResourceContext().withPageId("test")));
+
+ LearningResource resource = createEntity(request);
+ assertNotNull(resource.getId());
+ assertTrue(resource.getCategories().contains(category));
+ }
+ }
+
+ // ===================================================================
+ // RESOURCE TYPE TESTS
+ // ===================================================================
+
+ @Test
+ void post_learningResourceAllTypes_200_OK(TestNamespace ns) {
+ CreateLearningResource.ResourceType[] types = CreateLearningResource.ResourceType.values();
+
+ for (CreateLearningResource.ResourceType type : types) {
+ CreateLearningResource request =
+ new CreateLearningResource()
+ .withName(ns.prefix("type-" + type.value().toLowerCase()))
+ .withDescription("Resource type " + type.value())
+ .withResourceType(type)
+ .withCategories(List.of(ResourceCategory.DISCOVERY))
+ .withSource(
+ new LearningResourceSource()
+ .withUrl(URI.create("https://example.com/" + type.value())))
+ .withContexts(List.of(new LearningResourceContext().withPageId("explore")));
+
+ LearningResource resource = createEntity(request);
+ assertEquals(type.value(), resource.getResourceType().value());
+ }
+ }
+
+ // ===================================================================
+ // DIFFICULTY TESTS
+ // ===================================================================
+
+ @Test
+ void post_learningResourceAllDifficulties_200_OK(TestNamespace ns) {
+ CreateLearningResource.ResourceDifficulty[] difficulties =
+ CreateLearningResource.ResourceDifficulty.values();
+
+ for (CreateLearningResource.ResourceDifficulty difficulty : difficulties) {
+ CreateLearningResource request =
+ new CreateLearningResource()
+ .withName(ns.prefix("diff-" + difficulty.value().toLowerCase()))
+ .withDescription("Difficulty " + difficulty.value())
+ .withResourceType(CreateLearningResource.ResourceType.VIDEO)
+ .withCategories(List.of(ResourceCategory.DISCOVERY))
+ .withDifficulty(difficulty)
+ .withSource(
+ new LearningResourceSource()
+ .withUrl(URI.create("https://example.com/diff-" + difficulty.value())))
+ .withContexts(List.of(new LearningResourceContext().withPageId("explore")));
+
+ LearningResource resource = createEntity(request);
+ assertEquals(difficulty.value(), resource.getDifficulty().value());
+ }
+ }
+
+ // ===================================================================
+ // STATUS TRANSITION TESTS
+ // ===================================================================
+
+ @Test
+ void post_learningResourceStatusTransitions_200_OK(TestNamespace ns) {
+ CreateLearningResource request =
+ new CreateLearningResource()
+ .withName(ns.prefix("status-test"))
+ .withDescription("Status transition test")
+ .withResourceType(CreateLearningResource.ResourceType.ARTICLE)
+ .withCategories(List.of(ResourceCategory.DATA_GOVERNANCE))
+ .withStatus(CreateLearningResource.Status.DRAFT)
+ .withSource(
+ new LearningResourceSource().withUrl(URI.create("https://example.com/draft")))
+ .withContexts(List.of(new LearningResourceContext().withPageId("glossary")));
+
+ LearningResource resource = createEntity(request);
+ assertEquals(CreateLearningResource.Status.DRAFT.value(), resource.getStatus().value());
+
+ request.withStatus(CreateLearningResource.Status.ACTIVE);
+ LearningResource updated = getLearningResourceService().put(request);
+ assertEquals(CreateLearningResource.Status.ACTIVE.value(), updated.getStatus().value());
+
+ request.withStatus(CreateLearningResource.Status.DEPRECATED);
+ updated = getLearningResourceService().put(request);
+ assertEquals(CreateLearningResource.Status.DEPRECATED.value(), updated.getStatus().value());
+ }
+
+ // ===================================================================
+ // CONTEXT TESTS
+ // ===================================================================
+
+ @Test
+ void post_learningResourceWithMultipleContexts_200_OK(TestNamespace ns) {
+ CreateLearningResource request =
+ new CreateLearningResource()
+ .withName(ns.prefix("multi-context"))
+ .withDescription("Multiple contexts test")
+ .withResourceType(CreateLearningResource.ResourceType.VIDEO)
+ .withCategories(List.of(ResourceCategory.DISCOVERY))
+ .withSource(
+ new LearningResourceSource()
+ .withUrl(URI.create("https://example.com/multi-context")))
+ .withContexts(
+ List.of(
+ new LearningResourceContext().withPageId("explore").withPriority(1),
+ new LearningResourceContext().withPageId("table").withPriority(2),
+ new LearningResourceContext()
+ .withPageId("glossary")
+ .withComponentId("header")
+ .withPriority(3)));
+
+ LearningResource resource = createEntity(request);
+ assertEquals(3, resource.getContexts().size());
+ }
+
+ @Test
+ void post_learningResourceWithComponentId_200_OK(TestNamespace ns) {
+ CreateLearningResource request =
+ new CreateLearningResource()
+ .withName(ns.prefix("component-context"))
+ .withDescription("Component context test")
+ .withResourceType(CreateLearningResource.ResourceType.STORYLANE)
+ .withCategories(List.of(ResourceCategory.DATA_GOVERNANCE))
+ .withSource(
+ new LearningResourceSource()
+ .withUrl(URI.create("https://storylane.app/embed/component")))
+ .withContexts(
+ List.of(
+ new LearningResourceContext()
+ .withPageId("glossary")
+ .withComponentId("terms-table")
+ .withPriority(1)));
+
+ LearningResource resource = createEntity(request);
+ assertEquals("terms-table", resource.getContexts().get(0).getComponentId());
+ }
+
+ // ===================================================================
+ // OPTIONAL FIELDS TESTS
+ // ===================================================================
+
+ @Test
+ void post_learningResourceWithDisplayName_200_OK(TestNamespace ns) {
+ CreateLearningResource request =
+ new CreateLearningResource()
+ .withName(ns.prefix("display-name-test"))
+ .withDisplayName("Getting Started with Data Discovery")
+ .withDescription("Display name test")
+ .withResourceType(CreateLearningResource.ResourceType.VIDEO)
+ .withCategories(List.of(ResourceCategory.DISCOVERY))
+ .withSource(
+ new LearningResourceSource().withUrl(URI.create("https://example.com/display")))
+ .withContexts(List.of(new LearningResourceContext().withPageId("explore")));
+
+ LearningResource resource = createEntity(request);
+ assertEquals("Getting Started with Data Discovery", resource.getDisplayName());
+ }
+
+ @Test
+ void post_learningResourceWithProvider_200_OK(TestNamespace ns) {
+ CreateLearningResource request =
+ new CreateLearningResource()
+ .withName(ns.prefix("provider-test"))
+ .withDescription("Provider test")
+ .withResourceType(CreateLearningResource.ResourceType.VIDEO)
+ .withCategories(List.of(ResourceCategory.DISCOVERY))
+ .withSource(
+ new LearningResourceSource()
+ .withProvider("YouTube")
+ .withUrl(URI.create("https://youtube.com/watch?v=test")))
+ .withContexts(List.of(new LearningResourceContext().withPageId("explore")));
+
+ LearningResource resource = createEntity(request);
+ assertEquals("YouTube", resource.getSource().getProvider());
+ }
+
+ @Test
+ void post_learningResourceWithAllOptionalFields_200_OK(TestNamespace ns) {
+ CreateLearningResource request =
+ new CreateLearningResource()
+ .withName(ns.prefix("all-fields"))
+ .withDisplayName("Complete Tutorial")
+ .withDescription("A comprehensive guide to all features.")
+ .withResourceType(CreateLearningResource.ResourceType.STORYLANE)
+ .withCategories(
+ List.of(
+ ResourceCategory.DISCOVERY,
+ ResourceCategory.DATA_GOVERNANCE,
+ ResourceCategory.AI))
+ .withDifficulty(CreateLearningResource.ResourceDifficulty.INTERMEDIATE)
+ .withStatus(CreateLearningResource.Status.ACTIVE)
+ .withEstimatedDuration(600)
+ .withSource(
+ new LearningResourceSource()
+ .withProvider("Storylane")
+ .withUrl(URI.create("https://storylane.app/embed/complete")))
+ .withContexts(
+ List.of(
+ new LearningResourceContext().withPageId("explore").withPriority(1),
+ new LearningResourceContext()
+ .withPageId("glossary")
+ .withComponentId("header")
+ .withPriority(2)));
+
+ LearningResource resource = createEntity(request);
+ assertEquals("Complete Tutorial", resource.getDisplayName());
+ assertEquals("A comprehensive guide to all features.", resource.getDescription());
+ assertEquals(3, resource.getCategories().size());
+ assertEquals(
+ CreateLearningResource.ResourceDifficulty.INTERMEDIATE.value(),
+ resource.getDifficulty().value());
+ assertEquals(600, resource.getEstimatedDuration());
+ assertEquals(2, resource.getContexts().size());
+ }
+
+ // ===================================================================
+ // VALIDATION TESTS
+ // ===================================================================
+
+ @Test
+ void post_learningResourceWithoutRequiredFields_400(TestNamespace ns) {
+ assertThrows(
+ Exception.class,
+ () -> createEntity(new CreateLearningResource().withName(null)),
+ "Creating resource without name should fail");
+
+ assertThrows(
+ Exception.class,
+ () ->
+ createEntity(
+ new CreateLearningResource()
+ .withName(ns.prefix("no-type"))
+ .withCategories(List.of(ResourceCategory.DISCOVERY))
+ .withSource(
+ new LearningResourceSource()
+ .withUrl(URI.create("https://example.com/test")))
+ .withContexts(List.of(new LearningResourceContext().withPageId("test")))),
+ "Creating resource without resourceType should fail");
+
+ assertThrows(
+ Exception.class,
+ () ->
+ createEntity(
+ new CreateLearningResource()
+ .withName(ns.prefix("no-categories"))
+ .withResourceType(CreateLearningResource.ResourceType.VIDEO)
+ .withCategories(List.of())
+ .withSource(
+ new LearningResourceSource()
+ .withUrl(URI.create("https://example.com/test")))
+ .withContexts(List.of(new LearningResourceContext().withPageId("test")))),
+ "Creating resource with empty categories should fail");
+
+ assertThrows(
+ Exception.class,
+ () ->
+ createEntity(
+ new CreateLearningResource()
+ .withName(ns.prefix("no-contexts"))
+ .withResourceType(CreateLearningResource.ResourceType.VIDEO)
+ .withCategories(List.of(ResourceCategory.DISCOVERY))
+ .withSource(
+ new LearningResourceSource()
+ .withUrl(URI.create("https://example.com/test")))
+ .withContexts(List.of())),
+ "Creating resource with empty contexts should fail");
+ }
+
+ @Test
+ void post_learningResourceWithInvalidValues_400(TestNamespace ns) {
+ assertThrows(
+ Exception.class,
+ () ->
+ createEntity(
+ new CreateLearningResource()
+ .withName(ns.prefix("neg-duration"))
+ .withResourceType(CreateLearningResource.ResourceType.VIDEO)
+ .withCategories(List.of(ResourceCategory.DISCOVERY))
+ .withEstimatedDuration(-100)
+ .withSource(
+ new LearningResourceSource()
+ .withUrl(URI.create("https://example.com/test")))
+ .withContexts(List.of(new LearningResourceContext().withPageId("test")))),
+ "Creating resource with negative duration should fail");
+
+ assertThrows(
+ Exception.class,
+ () ->
+ createEntity(
+ new CreateLearningResource()
+ .withName(ns.prefix("neg-priority"))
+ .withResourceType(CreateLearningResource.ResourceType.VIDEO)
+ .withCategories(List.of(ResourceCategory.DISCOVERY))
+ .withSource(
+ new LearningResourceSource()
+ .withUrl(URI.create("https://example.com/test")))
+ .withContexts(
+ List.of(
+ new LearningResourceContext().withPageId("test").withPriority(-5)))),
+ "Creating resource with negative priority should fail");
+ }
+
+ @Test
+ void post_learningResourceDuplicateName_409(TestNamespace ns) {
+ String resourceName = ns.prefix("duplicate-test");
+
+ CreateLearningResource request =
+ new CreateLearningResource()
+ .withName(resourceName)
+ .withDescription("First resource")
+ .withResourceType(CreateLearningResource.ResourceType.VIDEO)
+ .withCategories(List.of(ResourceCategory.DISCOVERY))
+ .withSource(
+ new LearningResourceSource().withUrl(URI.create("https://example.com/dup1")))
+ .withContexts(List.of(new LearningResourceContext().withPageId("explore")));
+
+ createEntity(request);
+
+ CreateLearningResource duplicate =
+ new CreateLearningResource()
+ .withName(resourceName)
+ .withDescription("Duplicate resource")
+ .withResourceType(CreateLearningResource.ResourceType.ARTICLE)
+ .withCategories(List.of(ResourceCategory.DATA_QUALITY))
+ .withSource(
+ new LearningResourceSource().withUrl(URI.create("https://example.com/dup2")))
+ .withContexts(List.of(new LearningResourceContext().withPageId("dataQuality")));
+
+ assertThrows(Exception.class, () -> createEntity(duplicate), "Duplicate name should fail");
+ }
+
+ @Test
+ void post_learningResourceWithMaxLengthDisplayName_200_OK(TestNamespace ns) {
+ String maxDisplayName = "A".repeat(120);
+
+ CreateLearningResource request =
+ new CreateLearningResource()
+ .withName(ns.prefix("max-display"))
+ .withDisplayName(maxDisplayName)
+ .withDescription("Max length display name test")
+ .withResourceType(CreateLearningResource.ResourceType.VIDEO)
+ .withCategories(List.of(ResourceCategory.DISCOVERY))
+ .withSource(
+ new LearningResourceSource().withUrl(URI.create("https://example.com/maxlen")))
+ .withContexts(List.of(new LearningResourceContext().withPageId("explore")));
+
+ LearningResource resource = createEntity(request);
+ assertEquals(120, resource.getDisplayName().length());
+ }
+
+ @Test
+ void post_learningResourceExceedingDisplayNameLength_400(TestNamespace ns) {
+ String tooLongDisplayName = "A".repeat(121);
+
+ CreateLearningResource request =
+ new CreateLearningResource()
+ .withName(ns.prefix("too-long-display"))
+ .withDisplayName(tooLongDisplayName)
+ .withDescription("Too long display name test")
+ .withResourceType(CreateLearningResource.ResourceType.VIDEO)
+ .withCategories(List.of(ResourceCategory.DISCOVERY))
+ .withSource(
+ new LearningResourceSource().withUrl(URI.create("https://example.com/toolong")))
+ .withContexts(List.of(new LearningResourceContext().withPageId("explore")));
+
+ assertThrows(
+ Exception.class,
+ () -> createEntity(request),
+ "Display name exceeding 120 chars should fail");
+ }
+
+ // ===================================================================
+ // BOUNDARY VALUE TESTS
+ // ===================================================================
+
+ @Test
+ void post_learningResourceWithZeroValues_200_OK(TestNamespace ns) {
+ CreateLearningResource request =
+ new CreateLearningResource()
+ .withName(ns.prefix("zero-values"))
+ .withDescription("Zero values test")
+ .withResourceType(CreateLearningResource.ResourceType.ARTICLE)
+ .withCategories(List.of(ResourceCategory.DISCOVERY))
+ .withEstimatedDuration(0)
+ .withCompletionThreshold(0.0)
+ .withSource(
+ new LearningResourceSource().withUrl(URI.create("https://example.com/zero")))
+ .withContexts(
+ List.of(new LearningResourceContext().withPageId("explore").withPriority(0)));
+
+ LearningResource resource = createEntity(request);
+ assertEquals(0, resource.getEstimatedDuration());
+ assertEquals(0, resource.getContexts().get(0).getPriority());
+ }
+
+ // ===================================================================
+ // LIST TESTS
+ // ===================================================================
+
+ @Test
+ void test_listLearningResources(TestNamespace ns) {
+ CreateLearningResource request1 =
+ new CreateLearningResource()
+ .withName(ns.prefix("list-1"))
+ .withDescription("First resource")
+ .withResourceType(CreateLearningResource.ResourceType.VIDEO)
+ .withCategories(List.of(ResourceCategory.DISCOVERY))
+ .withSource(
+ new LearningResourceSource().withUrl(URI.create("https://example.com/list1")))
+ .withContexts(List.of(new LearningResourceContext().withPageId("explore")));
+
+ CreateLearningResource request2 =
+ new CreateLearningResource()
+ .withName(ns.prefix("list-2"))
+ .withDescription("Second resource")
+ .withResourceType(CreateLearningResource.ResourceType.ARTICLE)
+ .withCategories(List.of(ResourceCategory.DATA_QUALITY))
+ .withSource(
+ new LearningResourceSource().withUrl(URI.create("https://example.com/list2")))
+ .withContexts(List.of(new LearningResourceContext().withPageId("dataQuality")));
+
+ createEntity(request1);
+ createEntity(request2);
+
+ ListParams params = new ListParams();
+ params.setLimit(10);
+ ListResponse response = listEntities(params);
+
+ assertNotNull(response);
+ assertTrue(response.getData().size() >= 2);
+ }
+
+ // ===================================================================
+ // HELPER METHODS
+ // ===================================================================
+
+ private LearningResourceService getLearningResourceService() {
+ return new LearningResourceService(SdkClients.adminClient().getHttpClient());
+ }
+}
diff --git a/openmetadata-sdk/src/main/java/org/openmetadata/sdk/services/learning/LearningResourceService.java b/openmetadata-sdk/src/main/java/org/openmetadata/sdk/services/learning/LearningResourceService.java
new file mode 100644
index 000000000000..1272e4a8d551
--- /dev/null
+++ b/openmetadata-sdk/src/main/java/org/openmetadata/sdk/services/learning/LearningResourceService.java
@@ -0,0 +1,27 @@
+package org.openmetadata.sdk.services.learning;
+
+import org.openmetadata.schema.api.learning.CreateLearningResource;
+import org.openmetadata.schema.entity.learning.LearningResource;
+import org.openmetadata.sdk.exceptions.OpenMetadataException;
+import org.openmetadata.sdk.network.HttpClient;
+import org.openmetadata.sdk.network.HttpMethod;
+import org.openmetadata.sdk.services.EntityServiceBase;
+
+public class LearningResourceService extends EntityServiceBase {
+ public LearningResourceService(HttpClient httpClient) {
+ super(httpClient, "/v1/learning/resources");
+ }
+
+ @Override
+ protected Class getEntityClass() {
+ return LearningResource.class;
+ }
+
+ public LearningResource create(CreateLearningResource request) throws OpenMetadataException {
+ return httpClient.execute(HttpMethod.POST, basePath, request, LearningResource.class);
+ }
+
+ public LearningResource put(CreateLearningResource request) throws OpenMetadataException {
+ return httpClient.execute(HttpMethod.PUT, basePath, request, LearningResource.class);
+ }
+}
diff --git a/openmetadata-service/LEARNING_RESOURCES_DESIGN.md b/openmetadata-service/LEARNING_RESOURCES_DESIGN.md
new file mode 100644
index 000000000000..86582e2f29cc
--- /dev/null
+++ b/openmetadata-service/LEARNING_RESOURCES_DESIGN.md
@@ -0,0 +1,1142 @@
+# Learning Resources System - Design Document
+
+**Date:** 2025-12-26
+**Status:** Production Ready
+**Version:** 1.0 - Simplified (No Progress Tracking)
+
+---
+
+## Table of Contents
+
+1. [Overview](#overview)
+2. [Architecture](#architecture)
+3. [User Experience](#user-experience)
+4. [Backend Implementation](#backend-implementation)
+5. [Frontend Implementation](#frontend-implementation)
+6. [Data Model](#data-model)
+7. [API Endpoints](#api-endpoints)
+8. [Sample Resources](#sample-resources)
+9. [Integration Points](#integration-points)
+10. [What Was Removed](#what-was-removed)
+11. [Next Steps](#next-steps)
+
+---
+
+## Overview
+
+The Learning Resources system provides contextual, in-product learning materials to help users understand and use OpenMetadata features. Resources are matched to specific pages/components and displayed on-demand when users click a lightbulb icon.
+
+### Key Principles
+
+- **User-Initiated:** Resources only appear when users click the lightbulb (💡) icon - no automatic inline display
+- **Contextual:** Resources are matched to specific pages (glossary, domain, data products, etc.) and optional component IDs
+- **Multi-Format:** Supports Articles (markdown), Videos (YouTube/Vimeo), and Interactive Demos (Storylane)
+- **Simple:** No progress tracking, no badges, no gamification - just content delivery
+- **Admin-Managed:** Full CRUD interface for creating and managing learning resources
+
+---
+
+## Architecture
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ User Interface │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Page Header │
+│ ┌──────────────────────────────────────────────────┐ │
+│ │ [Page Title] 💡 (3) [Other Btns] │ │
+│ └──────────────────────────────────────────────────┘ │
+│ │ │
+│ │ Click │
+│ ▼ │
+│ ┌─────────────────────┐ │
+│ │ LearningDrawer │◄──── Fetches resources by pageId │
+│ │ (Side Panel) │ │
+│ ├─────────────────────┤ │
+│ │ 📚 Learning │ │
+│ │ Resources │ │
+│ ├─────────────────────┤ │
+│ │ • Resource 1 │ │
+│ │ • Resource 2 │ │
+│ │ • Resource 3 │ │
+│ └─────────────────────┘ │
+│ │ │
+│ │ Click Resource │
+│ ▼ │
+│ ┌────────────────────────────────────┐ │
+│ │ ResourcePlayerModal (Full Screen) │ │
+│ ├────────────────────────────────────┤ │
+│ │ [Article|Video|Storylane Player] │ │
+│ │ │ │
+│ │ Content displayed here... │ │
+│ │ │ │
+│ └────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## User Experience
+
+### Workflow
+
+1. **User browses** Glossary, Domain, or Data Product page
+2. **Sees lightbulb (💡) icon** in page header with badge count (e.g., "3")
+3. **Clicks lightbulb** → Side drawer opens from right
+4. **Sees list** of relevant learning resources (filtered by page context)
+5. **Clicks a resource** → Full modal opens with content player
+6. **Views content:**
+ - Article: Markdown rendered with full formatting
+ - Video: YouTube/Vimeo embedded player
+ - Storylane: Interactive demo iframe
+7. **Closes modal** when done
+8. **Can access** other resources from drawer
+
+### Key UX Decisions
+
+- ✅ **No automatic display** - Resources never appear without user action
+- ✅ **Badge count** - Shows number of available resources (e.g., 💡 with "3" badge)
+- ✅ **Side drawer pattern** - Non-intrusive, can stay open while browsing
+- ✅ **Full modal for content** - Immersive viewing experience
+- ✅ **No progress tracking** - Simple, no pressure to complete
+
+---
+
+## Backend Implementation
+
+### Entity: LearningResource
+
+**Location:** `openmetadata-spec/src/main/resources/json/schema/entity/learning/learningResource.json`
+
+**Key Fields:**
+- `name` (required): Unique identifier (e.g., "Intro_GlossaryBasics")
+- `displayName`: Human-readable title
+- `description`: Brief summary
+- `resourceType`: `Article` | `Video` | `Storylane`
+- `categories`: Array of categories (Discovery, DataGovernance, DataQuality, Administration, Observability)
+- `difficulty`: `Intro` | `Intermediate` | `Advanced`
+- `source`:
+ - `url`: Link to external content or video
+ - `provider`: Source name (e.g., "Collate", "OpenMetadata", "YouTube")
+ - `embedConfig`: Optional object for embedded content (used for Article markdown)
+- `estimatedDuration`: Duration in seconds
+- `contexts`: Array of page/component contexts where this resource applies
+ - `pageId`: Page identifier (e.g., "glossary", "domain", "dataProduct")
+ - `componentId`: Optional component identifier (e.g., "glossary-header", "metrics-tab")
+- `status`: `Draft` | `Active` | `Deprecated`
+- `owners`: Entity references for owners
+- `reviewers`: Entity references for reviewers
+
+**Example:**
+```json
+{
+ "name": "Intro_GlossaryBasics",
+ "displayName": "Glossary Basics: Building Your Business Vocabulary",
+ "description": "Learn the fundamentals of creating and managing glossaries...",
+ "resourceType": "Article",
+ "categories": ["Discovery", "DataGovernance"],
+ "difficulty": "Intro",
+ "source": {
+ "url": "https://www.getcollate.io/learning-center/resource/Intro_GlossaryBasics",
+ "provider": "Collate",
+ "embedConfig": {
+ "content": "# Glossary Basics\n\nMarkdown content here..."
+ }
+ },
+ "estimatedDuration": 720,
+ "contexts": [
+ {
+ "pageId": "glossary",
+ "componentId": "glossary-header"
+ }
+ ],
+ "status": "Active"
+}
+```
+
+### Repository
+
+**File:** `openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/LearningResourceRepository.java`
+
+**Key Methods:**
+- `prepare()`: Validates source URL, categories, contexts, and duration
+- `validateSource()`: Ensures valid URL format
+- `ensureCategories()`: Sets default Discovery category if none provided
+- `validateContexts()`: Ensures at least one context is provided
+- `validateDuration()`: Ensures non-negative duration
+- `listByContext()`: Filters resources by pageId and optional componentId
+
+**Validation Rules:**
+- Source URL is required and must be valid
+- At least one category required
+- At least one context (pageId) required
+- Duration must be >= 0 or null
+- ComponentId is optional within context
+
+### REST Resource
+
+**File:** `openmetadata-service/src/main/java/org/openmetadata/service/resources/learning/LearningResourceResource.java`
+
+**Endpoints:**
+- `POST /api/v1/learning/resources` - Create resource
+- `GET /api/v1/learning/resources` - List resources (with filtering)
+- `GET /api/v1/learning/resources/{id}` - Get by ID
+- `GET /api/v1/learning/resources/name/{fqn}` - Get by name
+- `PATCH /api/v1/learning/resources/{id}` - Update resource
+- `DELETE /api/v1/learning/resources/{id}` - Delete resource
+- `GET /api/v1/learning/resources/context/{pageId}` - Get by context (with optional componentId param)
+
+**Query Parameters for Context Filtering:**
+- `pageId`: Required - filter by page (e.g., "glossary")
+- `componentId`: Optional - further filter by component
+- `limit`: Max results to return
+- `fields`: Additional fields to include
+
+### Database Schema
+
+**Table:** `learning_resource_entity`
+
+**Migrations:**
+- MySQL: `bootstrap/sql/migrations/native/1.12.0/mysql/schemaChanges.sql`
+- PostgreSQL: `bootstrap/sql/migrations/native/1.12.0/postgres/schemaChanges.sql`
+
+**Note:** Badge and Progress tables were removed - they're in schema files but never created.
+
+---
+
+## Frontend Implementation
+
+### Components
+
+#### 1. LearningIcon (Lightbulb Button)
+
+**File:** `src/components/Learning/LearningIcon/LearningIcon.component.tsx`
+
+**Purpose:** Clickable lightbulb icon with resource count badge
+
+**Features:**
+- Shows badge with count of available resources
+- Lazy loads count on hover (optimization)
+- Opens LearningDrawer on click
+- Supports different sizes (small, medium, large)
+- Optional label text
+
+**Props:**
+```typescript
+interface LearningIconProps {
+ pageId: string;
+ componentId?: string;
+ className?: string;
+ size?: 'small' | 'medium' | 'large';
+ label?: string;
+ tooltip?: string;
+ placement?: 'top' | 'bottom' | 'left' | 'right';
+}
+```
+
+**Usage:**
+```tsx
+
+```
+
+#### 2. LearningDrawer (Side Panel)
+
+**File:** `src/components/Learning/LearningDrawer/LearningDrawer.component.tsx`
+
+**Purpose:** Side drawer that lists available resources
+
+**Features:**
+- Opens from right side
+- Shows title: "Learning Resources for {page}"
+- Lists resources as cards
+- Click card → opens ResourcePlayerModal
+- Auto-fetches resources when opened
+- Shows loading spinner while fetching
+- Shows empty state if no resources
+
+**Props:**
+```typescript
+interface LearningDrawerProps {
+ open: boolean;
+ pageId: string;
+ componentId?: string;
+ onClose: () => void;
+}
+```
+
+#### 3. ResourcePlayerModal (Content Viewer)
+
+**File:** `src/components/Learning/ResourcePlayer/ResourcePlayerModal.component.tsx`
+
+**Purpose:** Full-screen modal for displaying resource content
+
+**Features:**
+- Routes to correct player based on resourceType
+- Shows resource metadata (title, description, difficulty, duration, categories)
+- Clean close button
+- Responsive sizing
+
+**Player Routing:**
+```typescript
+switch (resource.resourceType) {
+ case 'Video': return ;
+ case 'Storylane': return ;
+ case 'Article': return ;
+}
+```
+
+#### 4. VideoPlayer (YouTube/Vimeo)
+
+**File:** `src/components/Learning/ResourcePlayer/VideoPlayer.component.tsx`
+
+**Purpose:** Embeds YouTube and Vimeo videos
+
+**Features:**
+- **Smart URL conversion:**
+ - `youtube.com/watch?v=abc123` → `youtube.com/embed/abc123`
+ - `youtu.be/abc123` → `youtube.com/embed/abc123`
+ - `vimeo.com/123456` → `player.vimeo.com/video/123456`
+- **16:9 responsive aspect ratio** (56.25% padding-bottom technique)
+- Loading spinner while video loads
+- Full permissions for video features (autoplay, fullscreen, etc.)
+
+**Styling:** `VideoPlayer.less` - Responsive container with padding-bottom aspect ratio
+
+#### 5. StorylaneTour (Interactive Demos)
+
+**File:** `src/components/Learning/ResourcePlayer/StorylaneTour.component.tsx`
+
+**Purpose:** Embeds Storylane interactive product demos
+
+**Features:**
+- Simple iframe embed
+- Fixed height (600px) suitable for demos
+- Loading spinner while demo loads
+- Fullscreen support
+- Clean, borderless presentation
+
+**URL Format:** `https://app.storylane.io/share/xxxxxxxxxx`
+
+**Styling:** `StorylaneTour.less` - Fixed height container
+
+#### 6. ArticleViewer (Markdown Content)
+
+**File:** `src/components/Learning/ResourcePlayer/ArticleViewer.component.tsx`
+
+**Purpose:** Renders markdown article content
+
+**Features:**
+- Uses existing `RichTextEditorPreviewer` component
+- Reads content from `resource.source.embedConfig.content`
+- No truncation (`enableSeeMoreVariant={false}`)
+- Full markdown support (headers, lists, code blocks, links, etc.)
+
+#### 7. LearningResourceCard
+
+**File:** `src/components/Learning/LearningResourceCard/LearningResourceCard.component.tsx`
+
+**Purpose:** Card UI for individual resource in lists
+
+**Features:**
+- Shows resource type icon
+- Displays title, description
+- Shows difficulty badge
+- Shows estimated duration
+- Shows categories as tags
+- Clickable to open resource
+
+### Admin UI
+
+#### 1. LearningResourcesPage (Management Table)
+
+**File:** `src/pages/LearningResourcesPage/LearningResourcesPage.tsx`
+
+**Purpose:** Admin interface for managing all learning resources
+
+**Features:**
+- **Table view** with columns:
+ - Name (displayName or name)
+ - Type (Article/Video/Storylane with colored tag)
+ - Difficulty (Intro/Intermediate/Advanced)
+ - Categories (multiple tags)
+ - Contexts (pageId:componentId pairs)
+ - Duration (converted to minutes)
+ - Status (Active/Draft/Deprecated)
+ - Actions (Preview, Edit, Delete buttons)
+- **Pagination** (20 per page, configurable)
+- **Create button** to add new resources
+- **Preview** - Opens ResourcePlayerModal to view resource
+- **Edit** - Opens LearningResourceForm with resource data
+- **Delete** - Shows confirmation modal before deletion
+
+**Access:** Need to add route to admin section
+
+#### 2. LearningResourceForm (Create/Edit Form)
+
+**File:** `src/pages/LearningResourcesPage/LearningResourceForm.component.tsx`
+
+**Purpose:** Modal form for creating or editing learning resources
+
+**Features:**
+- **Form Fields:**
+ - Name (required, unique identifier)
+ - Display Name
+ - Description (textarea)
+ - Resource Type (select: Article/Video/Storylane)
+ - Categories (multi-select)
+ - Difficulty (select: Intro/Intermediate/Advanced)
+ - Source URL (required, validated)
+ - Source Provider (text)
+ - **Embedded Content** (only for Article type) - Rich text editor for markdown
+ - Estimated Duration (number in minutes, converted to seconds)
+ - **Contexts** (dynamic Form.List):
+ - Page ID (select from predefined list)
+ - Component ID (optional text input)
+ - Add/Remove buttons for multiple contexts
+ - Status (select: Draft/Active/Deprecated)
+- **Validation:**
+ - Required fields enforced
+ - URL format validated
+ - At least one context required
+- **Submission:**
+ - Creates new resource or updates existing
+ - Converts duration minutes → seconds
+ - Handles embedded content for articles
+ - Shows success/error toasts
+
+**Constants in Form:**
+```typescript
+const RESOURCE_TYPES = ['Article', 'Video', 'Storylane'];
+const DIFFICULTIES = ['Intro', 'Intermediate', 'Advanced'];
+const CATEGORIES = ['Discovery', 'Administration', 'DataGovernance',
+ 'DataQuality', 'Observability'];
+const STATUSES = ['Draft', 'Active', 'Deprecated'];
+const PAGE_IDS = ['glossary', 'glossaryTerm', 'domain', 'dataProduct',
+ 'dataQuality', 'table', 'dashboard', 'pipeline', 'topic',
+ 'explore', 'governance'];
+```
+
+### API Integration
+
+**File:** `src/rest/learningResourceAPI.ts`
+
+**Key Functions:**
+```typescript
+// Create new resource
+export const createLearningResource = (data: CreateLearningResource): Promise
+
+// Get all resources (with optional filters)
+export const getLearningResourcesList = (params?: {
+ limit?: number;
+ fields?: string;
+ category?: string;
+ difficulty?: string;
+}): Promise<{ data: LearningResource[]; paging: Paging }>
+
+// Get resources by context
+export const getLearningResourcesByContext = (
+ pageId: string,
+ params?: {
+ componentId?: string;
+ limit?: number;
+ fields?: string;
+ }
+): Promise<{ data: LearningResource[]; paging: Paging }>
+
+// Update existing resource
+export const updateLearningResource = (id: string, data: CreateLearningResource): Promise
+
+// Delete resource
+export const deleteLearningResource = (id: string): Promise
+```
+
+---
+
+## Data Model
+
+### TypeScript Interfaces
+
+**File:** `src/rest/learningResourceAPI.ts`
+
+```typescript
+export interface LearningResource {
+ id: string;
+ name: string;
+ fullyQualifiedName?: string;
+ displayName?: string;
+ description?: string;
+ resourceType: 'Article' | 'Video' | 'Storylane';
+ categories: string[];
+ difficulty?: 'Intro' | 'Intermediate' | 'Advanced';
+ source: {
+ url: string;
+ provider?: string;
+ embedConfig?: {
+ content?: string; // For Article type - markdown content
+ [key: string]: unknown;
+ };
+ };
+ estimatedDuration?: number; // In seconds
+ contexts: Array<{
+ pageId: string;
+ componentId?: string;
+ }>;
+ status?: 'Draft' | 'Active' | 'Deprecated';
+ owners?: EntityReference[];
+ reviewers?: EntityReference[];
+ version?: number;
+ updatedAt?: number;
+ updatedBy?: string;
+ href?: string;
+}
+
+export interface CreateLearningResource {
+ name: string;
+ displayName?: string;
+ description?: string;
+ resourceType: 'Article' | 'Video' | 'Storylane';
+ categories: string[];
+ difficulty?: 'Intro' | 'Intermediate' | 'Advanced';
+ source: {
+ url: string;
+ provider?: string;
+ embedConfig?: {
+ content?: string;
+ [key: string]: unknown;
+ };
+ };
+ estimatedDuration?: number;
+ contexts: Array<{
+ pageId: string;
+ componentId?: string;
+ }>;
+ status?: 'Draft' | 'Active' | 'Deprecated';
+ owners?: EntityReference[];
+ reviewers?: EntityReference[];
+}
+```
+
+### Context Matching Logic
+
+Resources are matched to pages using a hierarchical system:
+
+1. **Exact match:** `pageId` + `componentId` both match
+2. **Page match:** `pageId` matches, no `componentId` specified in resource
+3. **Broad match:** Resource applies to entire page
+
+**Example:**
+- Resource with `pageId: "glossary", componentId: "glossary-header"` → Only shows in glossary header
+- Resource with `pageId: "glossary"` (no componentId) → Shows anywhere on glossary pages
+
+---
+
+## API Endpoints
+
+### Base URL
+`/api/v1/learning/resources`
+
+### Endpoints
+
+#### Create Resource
+```http
+POST /api/v1/learning/resources
+Content-Type: application/json
+
+{
+ "name": "Intro_GlossaryBasics",
+ "displayName": "Glossary Basics",
+ "resourceType": "Article",
+ "categories": ["Discovery"],
+ "source": {
+ "url": "https://example.com/resource",
+ "provider": "Collate"
+ },
+ "contexts": [
+ { "pageId": "glossary" }
+ ]
+}
+```
+
+#### List All Resources
+```http
+GET /api/v1/learning/resources?limit=100&fields=categories,contexts,difficulty
+```
+
+#### Get by Context
+```http
+GET /api/v1/learning/resources/context/glossary?componentId=glossary-header&limit=10
+```
+
+#### Get by ID
+```http
+GET /api/v1/learning/resources/{uuid}
+```
+
+#### Get by Name
+```http
+GET /api/v1/learning/resources/name/Intro_GlossaryBasics
+```
+
+#### Update Resource
+```http
+PATCH /api/v1/learning/resources/{uuid}
+Content-Type: application/json-patch+json
+
+[
+ {
+ "op": "replace",
+ "path": "/difficulty",
+ "value": "Advanced"
+ }
+]
+```
+
+#### Delete Resource
+```http
+DELETE /api/v1/learning/resources/{uuid}
+```
+
+---
+
+## Sample Resources
+
+### Current Resources (15 total)
+
+**Location:** `openmetadata-service/src/main/resources/json/data/learningResource/`
+
+#### Articles (14)
+
+**Intro Level (5):**
+1. `Intro_GlossaryBasics.json` - Glossary fundamentals
+2. `Intro_DomainManagement.json` - Domain organization
+3. `Intro_DataCatalogBasics.json` - Data catalog basics
+4. `Intro_DataLineage2025.json` - Understanding lineage
+5. `QuickStart_TableDiscovery.json` - Quick table discovery
+
+**Intermediate Level (5):**
+1. `Intermediate_GlossaryTerms.json` - Advanced glossary usage
+2. `Intermediate_DataGovernance.json` - Governance frameworks
+3. `Intermediate_DataGovernance2025.json` - Updated governance
+4. `Intermediate_DataProducts.json` - Data product patterns
+5. `Intermediate_DataQuality2025.json` - Data quality practices
+
+**Advanced Level (4):**
+1. `Advanced_DataQuality.json` - Data quality testing
+2. `Advanced_DataProductMetrics.json` - Product metrics
+3. `Advanced_DataObservability2025.json` - Observability patterns
+4. `Advanced_DomainStrategies.json` - Domain strategies
+
+#### Storylane Demos (1)
+
+1. `Demo_GettingStartedCollate.json` - **PLACEHOLDER** - needs actual Storylane URL
+
+### Resource Distribution by Category
+
+- **Discovery:** 5 resources
+- **DataGovernance:** 7 resources
+- **DataQuality:** 3 resources
+- **Administration:** 1 resource
+- **Observability:** 1 resource
+
+### Resource Distribution by Page Context
+
+- **glossary:** 6 resources
+- **glossaryTerm:** 4 resources
+- **domain:** 3 resources
+- **dataProduct:** 2 resources
+- **dataQuality:** 1 resource
+- **governance:** 1 resource
+- **explore:** 2 resources
+- **table:** 3 resources
+
+---
+
+## Integration Points
+
+### Current Integrations
+
+The LearningIcon is integrated in multiple components:
+
+#### 1. Data Assets Header (All Entity Pages)
+**File:** `src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx`
+
+```tsx
+
+ {/* Other buttons */}
+
+
+```
+
+**Applies to all data asset types:**
+- Table (`table`)
+- Pipeline (`pipeline`)
+- Dashboard (`dashboard`)
+- Topic (`topic`)
+- Container (`container`)
+- MlModel (`mlmodel`)
+- SearchIndex (`searchIndex`)
+- And other entity types...
+
+#### 2. Glossary Pages
+**File:** `src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx`
+
+```tsx
+
+
+ {/* Other buttons */}
+
+```
+
+**Resources shown:**
+- Glossary-related articles and tutorials
+- Dynamically switches between `glossary` and `glossaryTerm` context
+
+#### 3. Domain Pages
+**File:** `src/components/Domain/DomainDetails/DomainDetails.component.tsx`
+
+```tsx
+
+
+ {/* Other buttons */}
+
+```
+
+**Resources shown:**
+- Domain management guides
+- Domain strategy documents
+
+#### 4. Data Product Pages
+**File:** `src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx`
+
+```tsx
+
+
+ {/* Other buttons */}
+
+```
+
+**Resources shown:**
+- Data product creation guides
+- Data product metrics tutorials
+
+### Admin UI Route
+
+**Path:** `/settings/preferences/learning-resources`
+
+**File:** `src/components/AppRouter/SettingsRouter.tsx`
+
+**Menu Location:** Settings → Preferences → Learning Resources
+
+**Features:**
+- Full CRUD for learning resources
+- Preview resources in player modal
+- Manage contexts, categories, difficulty levels
+
+---
+
+## What Was Removed
+
+This section documents features that were removed during the simplification.
+
+### Removed Entities
+
+1. **LearningBadge**
+ - Schema: `openmetadata-spec/.../entity/learning/learningBadge.json`
+ - Repository: `LearningBadgeRepository.java`
+ - Resource: `LearningBadgeResource.java`
+ - Tests: `LearningBadgeResourceTest.java`
+ - Purpose: Gamification badges awarded for completing resources
+
+2. **LearningResourceProgress**
+ - Schema: `openmetadata-spec/.../entity/learning/learningResourceProgress.json`
+ - Repository: `LearningResourceProgressRepository.java`
+ - Resource: `LearningResourceProgressResource.java`
+ - Tests: `LearningResourceProgressResourceTest.java`
+ - Purpose: Track user progress through resources
+
+### Removed Fields from LearningResource
+
+- **completionThreshold** (Double) - Percentage required to mark as complete
+ - Removed from schema
+ - Removed from repository validation
+ - Removed from resource conversion
+ - Removed from tests
+ - Removed from all 14 sample JSON files
+
+### Removed Frontend Components/Features
+
+1. **Progress Tracking from Players:**
+ - VideoPlayer: Removed YouTube API message handlers, time tracking, progress intervals
+ - ArticleViewer: Removed scroll tracking, container refs, progress calculation
+ - StorylaneTour: Removed time-based progress tracking
+
+2. **Progress UI Components:**
+ - ResourcePlayerModal: Removed progress bar, completion percentage, "Mark Complete" button
+ - Removed `useLearningResourcePlayer` hook (entire file)
+
+3. **Inline Display Components:**
+ - LearningCenterBadge (created then removed - not needed)
+ - InlineLearningPanel (created then removed - not needed)
+
+### Removed Database Tables
+
+**Note:** These tables were defined in migration files but never actually created in the schema:
+- `learning_badge_entity`
+- `learning_resource_progress_entity`
+
+They were removed from migration files:
+- `bootstrap/sql/migrations/native/1.10.5/mysql/schemaChanges.sql`
+- `bootstrap/sql/migrations/native/1.10.5/postgres/schemaChanges.sql`
+
+### Removed API Endpoints
+
+- All badge-related endpoints (`/api/v1/learning/badges/*`)
+- All progress-related endpoints (`/api/v1/learning/progress/*`)
+
+---
+
+## Next Steps
+
+### Immediate (When Ready)
+
+1. **Add Storylane URLs**
+ - Get actual Storylane embed URLs from Collate learning center
+ - Replace `PLACEHOLDER_URL` in `Demo_GettingStartedCollate.json`
+ - Format: `https://app.storylane.io/share/xxxxxxxxxx`
+
+2. **Add More Demos**
+ - Create additional Storylane resources from learning center:
+ - Data Governance demo
+ - Data Lineage demo
+ - Data Quality demo
+ - Table Discovery demo
+ - Use `Demo_GettingStartedCollate.json` as template
+
+### Completed ✅
+
+1. **Admin Route Added**
+ - Route: `/settings/preferences/learning-resources`
+ - Location: Settings → Preferences → Learning Resources
+ - Admin-only access
+
+2. **LearningIcon Coverage Expanded**
+ - Added to DataAssetsHeader (covers all data asset entity pages)
+ - Now shows on: Table, Pipeline, Dashboard, Topic, Container, etc.
+
+### Optional Enhancements
+
+2. **Resource Management**
+ - Review and update article content as needed
+ - Ensure external links remain valid
+ - Update descriptions for clarity
+ - Add more resources for underserved contexts
+
+3. **Metrics (Future)**
+ - Track which resources are viewed most
+ - Identify popular topics
+ - Inform content creation priorities
+ - (Can be done via backend logging, no UI changes needed)
+
+4. **Search (Future)**
+ - Add search box in LearningDrawer
+ - Filter resources by keyword
+ - Search across title, description, categories
+
+5. **Resource Suggestions (Future)**
+ - Recommend related resources
+ - "If you liked X, try Y"
+ - Based on category/difficulty
+
+---
+
+## Technical Notes
+
+### Build Commands
+
+```bash
+# Backend
+mvn clean compile -pl openmetadata-service -DskipTests
+mvn spotless:apply -pl openmetadata-service
+mvn test-compile -pl openmetadata-service
+
+# Frontend
+cd openmetadata-ui/src/main/resources/ui
+yarn lint:fix
+yarn build
+```
+
+### File Locations Reference
+
+**Backend:**
+- Entity schema: `openmetadata-spec/src/main/resources/json/schema/entity/learning/learningResource.json`
+- Repository: `openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/LearningResourceRepository.java`
+- Resource: `openmetadata-service/src/main/java/org/openmetadata/service/resources/learning/LearningResourceResource.java`
+- Tests: `openmetadata-service/src/test/java/org/openmetadata/service/resources/learning/LearningResourceResourceTest.java`
+- Sample data: `openmetadata-service/src/main/resources/json/data/learningResource/*.json`
+- Migrations: `bootstrap/sql/migrations/native/1.12.0/{mysql,postgres}/schemaChanges.sql`
+
+**Frontend:**
+- API client: `src/rest/learningResourceAPI.ts`
+- LearningIcon: `src/components/Learning/LearningIcon/`
+- LearningDrawer: `src/components/Learning/LearningDrawer/`
+- ResourcePlayerModal: `src/components/Learning/ResourcePlayer/ResourcePlayerModal.component.tsx`
+- VideoPlayer: `src/components/Learning/ResourcePlayer/VideoPlayer.component.tsx`
+- StorylaneTour: `src/components/Learning/ResourcePlayer/StorylaneTour.component.tsx`
+- ArticleViewer: `src/components/Learning/ResourcePlayer/ArticleViewer.component.tsx`
+- ResourceCard: `src/components/Learning/LearningResourceCard/`
+- Admin UI: `src/pages/LearningResourcesPage/`
+
+### Dependencies
+
+**Backend:**
+- Standard OpenMetadata entity framework
+- JDBI3 for database access
+- Jackson for JSON serialization
+- Standard REST resource patterns
+
+**Frontend:**
+- React + TypeScript
+- Ant Design (Table, Form, Modal, Drawer, Badge, Button, etc.)
+- react-i18next for translations
+- RichTextEditorPreviewer for markdown (existing OM component)
+
+### Translation Keys Used
+
+**Existing Keys (already in codebase):**
+- `label.learning-resources`
+- `label.learning-resource`
+- `label.difficulty`
+- `label.category-plural`
+- `label.context-plural`
+- `label.duration`
+- `label.status`
+- `label.create`
+- `label.update`
+- `label.delete`
+- `label.preview`
+- `label.close`
+- `message.learning-resources-available` (with count parameter)
+- `message.resources-available` (with count parameter)
+- `message.no-learning-resources-available`
+
+**May Need to Add:**
+- `label.resource-lowercase-plural`
+- `label.learning-resources-for` (with context parameter)
+- `label.component-id-optional`
+- `label.page-id`
+- `label.embedded-content`
+- `label.source-url`
+- `label.source-provider`
+- `label.estimated-duration-minutes`
+- `message.optional-markdown-content`
+- `message.write-markdown-content`
+- `server.learning-resources-fetch-error`
+
+---
+
+## Design Decisions & Rationale
+
+### Why No Progress Tracking?
+
+**Original Design:** Complex system with progress tracking, completion thresholds, badges, gamification.
+
+**Simplified Design:** Just content delivery.
+
+**Reasons:**
+1. **Complexity vs Value:** Progress tracking added significant complexity with unclear value
+2. **User Pressure:** Users felt pressured to "complete" resources rather than learn
+3. **Privacy Concerns:** Tracking user behavior can raise privacy issues
+4. **Simplicity:** Easier to maintain, easier to understand, easier to use
+5. **Content Focus:** Focus on quality content, not gamification
+
+### Why Lightbulb Icon (User-Initiated)?
+
+**Alternatives considered:**
+- Auto-show banner at top of page
+- Inline panels in page content
+- Floating widget in corner
+
+**Chosen: Lightbulb in header**
+
+**Reasons:**
+1. **Non-intrusive:** Doesn't block or clutter the main content
+2. **Discoverable:** Visible but not annoying
+3. **User Control:** Users choose when to learn
+4. **Familiar Pattern:** Similar to help/docs icons
+5. **Badge Count:** Shows availability without requiring click
+
+### Why Side Drawer vs Modal?
+
+**For resource list:** Side drawer
+**For content:** Full modal
+
+**Reasons:**
+1. **Drawer Benefits:**
+ - Can stay open while user browses
+ - Non-blocking for main content
+ - Shows context (what page you're on)
+ - Easy to dismiss but also easy to keep open
+
+2. **Modal Benefits (for content):**
+ - Immersive viewing experience
+ - Full screen for videos/demos
+ - Focused attention on content
+ - Clear "viewing mode" vs "browsing mode"
+
+### Why Three Resource Types?
+
+**Article, Video, Storylane** - why not more?
+
+**Reasoning:**
+1. **Article (Markdown):**
+ - Self-contained, searchable, version-controllable
+ - Fast to load, accessible, printable
+ - Good for reference documentation
+
+2. **Video (YouTube/Vimeo):**
+ - Engaging, visual, personality
+ - Good for tutorials and overviews
+ - Familiar platform
+
+3. **Storylane (Interactive Demo):**
+ - Hands-on experience without setup
+ - Safe to explore (can't break anything)
+ - Shows actual product UI
+
+**Not included:**
+- PDFs: Hard to read in browser, not responsive
+- External links: Jarring experience leaving product
+- Webinars: Too long, scheduling issues
+
+---
+
+## Success Metrics (Future)
+
+While we don't track progress, we can measure success:
+
+### Adoption Metrics
+- Number of resources created
+- Number of pages with LearningIcon integrated
+- Diversity of contexts covered
+
+### Quality Metrics
+- % of resources with clear descriptions
+- % of resources with owners
+- % of critical features covered by resources
+
+### Potential Usage Metrics (if instrumented)
+- Lightbulb click rate
+- Resource view counts
+- Average time in ResourcePlayerModal
+- Return visits to same resource
+
+---
+
+## Version History
+
+### v1.0 - Current (2025-12-26)
+- Simplified design with no progress tracking
+- LearningIcon + LearningDrawer pattern
+- Three resource types: Article, Video, Storylane
+- Admin UI for resource management
+- 15 sample resources (14 articles, 1 demo placeholder)
+- Integrated in Glossary, Domain, Data Product pages
+
+### v0.x - Previous (Removed)
+- Complex progress tracking system
+- Badge/gamification system
+- Completion thresholds
+- Auto-displaying inline panels
+- Manual "Mark Complete" buttons
+
+---
+
+## Appendix: Code Snippets
+
+### Adding LearningIcon to a New Page
+
+```tsx
+import { LearningIcon } from 'components/Learning/LearningIcon/LearningIcon.component';
+
+// In your page header component:
+
+
+ {/* other header buttons */}
+
+```
+
+### Creating a New Resource via Admin UI
+
+1. Navigate to Learning Resources admin page
+2. Click "Add Resource" button
+3. Fill in form:
+ - Name: `Intermediate_YourFeature`
+ - Display Name: "Your Feature Guide"
+ - Description: Brief summary
+ - Type: Article/Video/Storylane
+ - Categories: Select relevant categories
+ - Difficulty: Intro/Intermediate/Advanced
+ - Source URL: External link or YouTube/Storylane URL
+ - (If Article) Embedded Content: Markdown text
+ - Duration: Estimated minutes
+ - Contexts: Add pageId(s) where this should appear
+ - Status: Active
+4. Click "Create"
+
+### Creating a Resource via JSON
+
+```json
+{
+ "name": "Intermediate_MyFeature",
+ "displayName": "Understanding My Feature",
+ "description": "A comprehensive guide to using My Feature effectively",
+ "resourceType": "Article",
+ "categories": ["Discovery", "DataGovernance"],
+ "difficulty": "Intermediate",
+ "source": {
+ "url": "https://www.getcollate.io/learning-center/my-feature",
+ "provider": "Collate",
+ "embedConfig": {
+ "content": "# Understanding My Feature\n\nMarkdown content here..."
+ }
+ },
+ "estimatedDuration": 600,
+ "contexts": [
+ {
+ "pageId": "my-feature-page",
+ "componentId": "feature-header"
+ }
+ ],
+ "status": "Active",
+ "owners": []
+}
+```
+
+Save to: `openmetadata-service/src/main/resources/json/data/learning/resource/Intermediate_MyFeature.json`
+
+---
+
+## Support & Questions
+
+For questions about this system:
+
+1. **Design Questions:** Refer to this document
+2. **Implementation Questions:** Check source files referenced in "File Locations"
+3. **Bug Reports:** Standard OpenMetadata issue process
+4. **Feature Requests:** Discuss with team, update "Next Steps" section
+
+---
+
+**End of Document**
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java b/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java
index b8c77358aef3..b457b0b3bba7 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java
@@ -294,6 +294,7 @@ public final class Entity {
public static final String ALL_RESOURCES = "All";
public static final String DOCUMENT = "document";
+ public static final String LEARNING_RESOURCE = "learningResource";
// ServiceType - Service Entity name map
static final Map SERVICE_TYPE_ENTITY_MAP = new EnumMap<>(ServiceType.class);
// entity type to service entity name map
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 f08773dbb824..f02259287842 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
@@ -121,6 +121,7 @@
import org.openmetadata.schema.entity.events.FailedEvent;
import org.openmetadata.schema.entity.events.FailedEventResponse;
import org.openmetadata.schema.entity.events.NotificationTemplate;
+import org.openmetadata.schema.entity.learning.LearningResource;
import org.openmetadata.schema.entity.policies.Policy;
import org.openmetadata.schema.entity.services.ApiService;
import org.openmetadata.schema.entity.services.DashboardService;
@@ -417,6 +418,9 @@ public interface CollectionDAO {
@CreateSqlObject
DocStoreDAO docStoreDAO();
+ @CreateSqlObject
+ LearningResourceDAO learningResourceDAO();
+
@CreateSqlObject
SuggestionDAO suggestionDAO();
@@ -8380,6 +8384,23 @@ List listAfter(
void deleteEmailTemplates();
}
+ interface LearningResourceDAO extends EntityDAO {
+ @Override
+ default String getTableName() {
+ return "learning_resource_entity";
+ }
+
+ @Override
+ default Class getEntityClass() {
+ return LearningResource.class;
+ }
+
+ @Override
+ default String getNameHashColumn() {
+ return "fqnHash";
+ }
+ }
+
interface SuggestionDAO {
default String getTableName() {
return "suggestions";
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/LearningResourceRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/LearningResourceRepository.java
new file mode 100644
index 000000000000..80b5cfcc15b1
--- /dev/null
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/LearningResourceRepository.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright 2024 Collate
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmetadata.service.jdbi3;
+
+import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
+import static org.openmetadata.service.Entity.ADMIN_USER_NAME;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.openmetadata.schema.api.learning.ResourceCategory;
+import org.openmetadata.schema.entity.learning.LearningResource;
+import org.openmetadata.schema.entity.learning.LearningResourceContext;
+import org.openmetadata.schema.entity.learning.LearningResourceSource;
+import org.openmetadata.schema.type.Include;
+import org.openmetadata.schema.type.change.ChangeSource;
+import org.openmetadata.service.Entity;
+import org.openmetadata.service.exception.BadRequestException;
+import org.openmetadata.service.resources.databases.DatasourceConfig;
+import org.openmetadata.service.resources.learning.LearningResourceResource;
+import org.openmetadata.service.util.EntityUtil.Fields;
+import org.openmetadata.service.util.EntityUtil.RelationIncludes;
+import org.openmetadata.service.util.FullyQualifiedName;
+
+@Slf4j
+public class LearningResourceRepository extends EntityRepository {
+ private static final String UPDATE_FIELDS =
+ "owners,reviewers,tags,contexts,categories,difficulty,source,estimatedDuration,status";
+ private static final String PATCH_FIELDS = UPDATE_FIELDS;
+
+ public LearningResourceRepository() {
+ super(
+ LearningResourceResource.COLLECTION_PATH,
+ Entity.LEARNING_RESOURCE,
+ LearningResource.class,
+ Entity.getCollectionDAO().learningResourceDAO(),
+ UPDATE_FIELDS,
+ PATCH_FIELDS);
+ supportsSearch = false;
+ }
+
+ /**
+ * Initialize seed data with merge/update support. Unlike the default initializeEntity which skips
+ * existing entities, this method will update existing resources if the seed data has changed.
+ */
+ public void initSeedDataWithMerge() throws java.io.IOException {
+ List seedEntities = getEntitiesFromSeedData();
+ for (LearningResource seedEntity : seedEntities) {
+ setFullyQualifiedName(seedEntity);
+ LearningResource existingEntity =
+ findByNameOrNull(seedEntity.getFullyQualifiedName(), Include.ALL);
+
+ if (existingEntity == null) {
+ // New entity - create it
+ LOG.info("Creating new learning resource: {}", seedEntity.getName());
+ seedEntity.setUpdatedBy(ADMIN_USER_NAME);
+ seedEntity.setUpdatedAt(System.currentTimeMillis());
+ seedEntity.setId(java.util.UUID.randomUUID());
+ create(null, seedEntity);
+ } else {
+ // Existing entity - check if update is needed by comparing key fields
+ boolean needsUpdate = hasChanges(existingEntity, seedEntity);
+ if (needsUpdate) {
+ LOG.info("Updating learning resource: {}", seedEntity.getName());
+ seedEntity.setId(existingEntity.getId());
+ seedEntity.setUpdatedBy(ADMIN_USER_NAME);
+ seedEntity.setUpdatedAt(System.currentTimeMillis());
+ seedEntity.setVersion(existingEntity.getVersion());
+ createOrUpdate(null, seedEntity, ADMIN_USER_NAME);
+ } else {
+ LOG.debug("Learning resource {} is up to date", seedEntity.getName());
+ }
+ }
+ }
+ }
+
+ private boolean hasChanges(LearningResource existing, LearningResource seed) {
+ // Compare fields that matter for seed data updates
+ if (!java.util.Objects.equals(existing.getDisplayName(), seed.getDisplayName())) return true;
+ if (!java.util.Objects.equals(existing.getDescription(), seed.getDescription())) return true;
+ if (!java.util.Objects.equals(existing.getResourceType(), seed.getResourceType())) return true;
+ if (!java.util.Objects.equals(existing.getCategories(), seed.getCategories())) return true;
+ if (!java.util.Objects.equals(existing.getContexts(), seed.getContexts())) return true;
+ if (!java.util.Objects.equals(existing.getDifficulty(), seed.getDifficulty())) return true;
+ if (!java.util.Objects.equals(existing.getSource(), seed.getSource())) return true;
+ if (!java.util.Objects.equals(existing.getStatus(), seed.getStatus())) return true;
+ return false;
+ }
+
+ @Override
+ protected void setFields(
+ LearningResource entity, Fields fields, RelationIncludes relationIncludes) {
+ // No additional field resolution for now
+ }
+
+ @Override
+ public void setFieldsInBulk(Fields fields, List entities) {
+ super.setFieldsInBulk(fields, entities);
+ }
+
+ @Override
+ protected void clearFields(LearningResource entity, Fields fields) {
+ // No-op
+ }
+
+ @Override
+ public void setFullyQualifiedName(LearningResource entity) {
+ if (StringUtils.isNotBlank(entity.getFullyQualifiedName())) {
+ return;
+ }
+ entity.setFullyQualifiedName(FullyQualifiedName.build(entity.getName()));
+ }
+
+ @Override
+ public void prepare(LearningResource entity, boolean update) {
+ validateSource(entity.getSource());
+ ensureCategories(entity);
+ validateContexts(entity.getContexts());
+ validateDuration(entity.getEstimatedDuration());
+ }
+
+ @Override
+ public void storeEntity(LearningResource entity, boolean update) {
+ store(entity, update);
+ }
+
+ @Override
+ public void storeRelationships(LearningResource entity) {
+ // All relationships handled centrally (owners, reviewers, tags)
+ }
+
+ public EntityRepository.EntityUpdater getUpdater(
+ LearningResource original,
+ LearningResource updated,
+ Operation operation,
+ ChangeSource changeSource) {
+ return new LearningResourceUpdater(original, updated, operation);
+ }
+
+ private void validateSource(LearningResourceSource source) {
+ if (source == null || source.getUrl() == null) {
+ throw BadRequestException.of("Learning resource source with URL is required");
+ }
+ }
+
+ private void ensureCategories(LearningResource entity) {
+ List categories = entity.getCategories();
+ if (nullOrEmpty(categories)) {
+ throw BadRequestException.of("Learning resource must include at least one category");
+ }
+ Set unique = new LinkedHashSet<>(categories);
+ if (unique.size() != categories.size()) {
+ entity.setCategories(new ArrayList<>(unique));
+ }
+ }
+
+ private void validateContexts(List contexts) {
+ if (nullOrEmpty(contexts)) {
+ throw BadRequestException.of("Learning resource requires at least one placement context");
+ }
+
+ Set uniqueKeys = new HashSet<>();
+ for (LearningResourceContext context : contexts) {
+ if (context == null || StringUtils.isBlank(context.getPageId())) {
+ throw BadRequestException.of("Learning resource context requires a non-empty pageId");
+ }
+ String componentId = StringUtils.defaultIfBlank(context.getComponentId(), "");
+ String key = context.getPageId() + "::" + componentId;
+ if (!uniqueKeys.add(key)) {
+ throw BadRequestException.of(
+ "Duplicate learning resource context for pageId '%s' and componentId '%s'"
+ .formatted(context.getPageId(), componentId));
+ }
+ }
+ }
+
+ private void validateDuration(Integer estimatedDuration) {
+ if (estimatedDuration != null && estimatedDuration < 0) {
+ throw BadRequestException.of("Estimated duration must be zero or a positive integer");
+ }
+ }
+
+ public static class LearningResourceFilter extends ListFilter {
+ public LearningResourceFilter(Include include) {
+ super(include);
+ }
+
+ @Override
+ public String getCondition(String tableName) {
+ String baseCondition = super.getCondition(tableName);
+ String placementCondition = buildPlacementCondition(tableName);
+ if (placementCondition.isEmpty()) {
+ return baseCondition;
+ }
+ if ("WHERE TRUE".equals(baseCondition) || baseCondition.isEmpty()) {
+ return "WHERE " + placementCondition;
+ }
+ return baseCondition + " AND " + placementCondition;
+ }
+
+ private String buildPlacementCondition(String tableName) {
+ List conditions = new ArrayList<>();
+ if (getQueryParam("pageId") != null) {
+ conditions.add(pageCondition(tableName));
+ }
+ if (getQueryParam("componentId") != null) {
+ conditions.add(componentCondition(tableName));
+ }
+ if (getQueryParam("category") != null) {
+ conditions.add(categoryCondition(tableName));
+ }
+ if (getQueryParam("difficulty") != null) {
+ conditions.add(difficultyCondition(tableName));
+ }
+ return conditions.isEmpty() ? "" : addCondition(conditions);
+ }
+
+ private String jsonColumn(String tableName) {
+ return tableName == null ? "json" : tableName + ".json";
+ }
+
+ private String pageCondition(String tableName) {
+ String column = jsonColumn(tableName);
+ if (Boolean.TRUE.equals(DatasourceConfig.getInstance().isMySQL())) {
+ return String.format(
+ "JSON_SEARCH(%s, 'one', :pageId, NULL, '$.contexts[*].pageId') IS NOT NULL", column);
+ }
+ return String.format(
+ "EXISTS (SELECT 1 FROM jsonb_array_elements(COALESCE(%s->'contexts', '[]'::jsonb)) ctx"
+ + " WHERE ctx->>'pageId' = :pageId)",
+ column);
+ }
+
+ private String componentCondition(String tableName) {
+ String column = jsonColumn(tableName);
+ if (Boolean.TRUE.equals(DatasourceConfig.getInstance().isMySQL())) {
+ return String.format(
+ "JSON_SEARCH(%s, 'one', :componentId, NULL, '$.contexts[*].componentId') IS NOT NULL",
+ column);
+ }
+ return String.format(
+ "EXISTS (SELECT 1 FROM jsonb_array_elements(COALESCE(%s->'contexts', '[]'::jsonb)) ctx"
+ + " WHERE ctx->>'componentId' = :componentId)",
+ column);
+ }
+
+ private String categoryCondition(String tableName) {
+ String column = jsonColumn(tableName);
+ if (Boolean.TRUE.equals(DatasourceConfig.getInstance().isMySQL())) {
+ return String.format(
+ "JSON_SEARCH(%s, 'one', :category, NULL, '$.categories') IS NOT NULL", column);
+ }
+ return String.format(
+ "EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(%s->'categories', '[]'::jsonb)) cat"
+ + " WHERE cat = :category)",
+ column);
+ }
+
+ private String difficultyCondition(String tableName) {
+ String column = jsonColumn(tableName);
+ if (Boolean.TRUE.equals(DatasourceConfig.getInstance().isMySQL())) {
+ return String.format(
+ "JSON_UNQUOTE(JSON_EXTRACT(%s, '$.difficulty')) = :difficulty", column);
+ }
+ return String.format("%s->>'difficulty' = :difficulty", column);
+ }
+ }
+
+ class LearningResourceUpdater extends EntityUpdater {
+ LearningResourceUpdater(
+ LearningResource original, LearningResource updated, Operation operation) {
+ super(original, updated, operation);
+ }
+
+ @Override
+ public void entitySpecificUpdate(boolean consolidatingChanges) {
+ recordChange("categories", original.getCategories(), updated.getCategories());
+ recordChange("contexts", original.getContexts(), updated.getContexts(), true);
+ recordChange("difficulty", original.getDifficulty(), updated.getDifficulty());
+ recordChange("source", original.getSource(), updated.getSource(), true);
+ recordChange(
+ "estimatedDuration", original.getEstimatedDuration(), updated.getEstimatedDuration());
+ }
+ }
+}
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/learning/LearningResourceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/learning/LearningResourceResource.java
new file mode 100644
index 000000000000..9bf5d24d59dd
--- /dev/null
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/learning/LearningResourceResource.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright 2024 Collate
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmetadata.service.resources.learning;
+
+import io.swagger.v3.oas.annotations.ExternalDocumentation;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.ExampleObject;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.json.JsonPatch;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PATCH;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.SecurityContext;
+import jakarta.ws.rs.core.UriInfo;
+import java.io.IOException;
+import java.util.UUID;
+import lombok.extern.slf4j.Slf4j;
+import org.openmetadata.schema.api.data.RestoreEntity;
+import org.openmetadata.schema.api.learning.CreateLearningResource;
+import org.openmetadata.schema.entity.learning.LearningResource;
+import org.openmetadata.schema.type.EntityHistory;
+import org.openmetadata.schema.type.Include;
+import org.openmetadata.schema.utils.ResultList;
+import org.openmetadata.service.Entity;
+import org.openmetadata.service.OpenMetadataApplicationConfig;
+import org.openmetadata.service.jdbi3.LearningResourceRepository;
+import org.openmetadata.service.limits.Limits;
+import org.openmetadata.service.resources.Collection;
+import org.openmetadata.service.resources.EntityResource;
+import org.openmetadata.service.security.Authorizer;
+
+@Slf4j
+@Path("/v1/learning/resources")
+@Tag(
+ name = "Learning Resources",
+ description =
+ "Inline tutorials and expert content surfaced across OpenMetadata product surfaces.")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+@Collection(name = "learning/resources", order = 3)
+public class LearningResourceResource
+ extends EntityResource {
+ public static final String COLLECTION_PATH = "/v1/learning/resources";
+ static final String FIELDS = "owners,reviewers,tags,followers,contexts,categories";
+
+ public LearningResourceResource(Authorizer authorizer, Limits limits) {
+ super(Entity.LEARNING_RESOURCE, authorizer, limits);
+ }
+
+ @Override
+ public void initialize(OpenMetadataApplicationConfig config) throws IOException {
+ // Use merge method to add new resources and update existing ones
+ repository.initSeedDataWithMerge();
+ }
+
+ @Override
+ public LearningResource addHref(UriInfo uriInfo, LearningResource resource) {
+ super.addHref(uriInfo, resource);
+ return resource;
+ }
+
+ public static class LearningResourceList extends ResultList {
+ /* Required for serde */
+ }
+
+ @GET
+ @Operation(
+ operationId = "listLearningResources",
+ summary = "List learning resources",
+ description = "Get a paginated list of learning resources with optional contextual filters.",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "List of learning resources",
+ content =
+ @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = LearningResourceList.class)))
+ })
+ public ResultList list(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(
+ description = "Fields requested in the returned resource",
+ schema = @Schema(type = "string", example = FIELDS))
+ @QueryParam("fields")
+ String fieldsParam,
+ @Parameter(description = "Limit the number of results returned. (1 to 1000000, default = 10)")
+ @DefaultValue("10")
+ @Min(0)
+ @Max(1000000)
+ @QueryParam("limit")
+ int limitParam,
+ @Parameter(description = "Returns list of learning resources before this cursor")
+ @QueryParam("before")
+ String before,
+ @Parameter(description = "Returns list of learning resources after this cursor")
+ @QueryParam("after")
+ String after,
+ @Parameter(
+ description = "Include all, deleted, or non-deleted entities",
+ schema = @Schema(implementation = Include.class))
+ @QueryParam("include")
+ @DefaultValue("non-deleted")
+ Include include,
+ @Parameter(
+ description = "Filter resources to a specific page identifier",
+ schema = @Schema(type = "string"))
+ @QueryParam("pageId")
+ String pageId,
+ @Parameter(
+ description = "Filter by component identifier within a page",
+ schema = @Schema(type = "string"))
+ @QueryParam("componentId")
+ String componentId,
+ @Parameter(
+ description = "Filter by primary category",
+ schema = @Schema(type = "string", example = "DataGovernance"))
+ @QueryParam("category")
+ String category,
+ @Parameter(
+ description = "Filter by difficulty tier",
+ schema = @Schema(type = "string", example = "Intro"))
+ @QueryParam("difficulty")
+ String difficulty,
+ @Parameter(
+ description = "Filter by lifecycle status",
+ schema = @Schema(type = "string", example = "Active"))
+ @QueryParam("status")
+ String status) {
+ LearningResourceRepository.LearningResourceFilter filter =
+ new LearningResourceRepository.LearningResourceFilter(include);
+ if (pageId != null) {
+ filter.addQueryParam("pageId", pageId);
+ }
+ if (componentId != null) {
+ filter.addQueryParam("componentId", componentId);
+ }
+ if (category != null) {
+ filter.addQueryParam("category", category);
+ }
+ if (difficulty != null) {
+ filter.addQueryParam("difficulty", difficulty);
+ }
+ if (status != null) {
+ filter.addQueryParam("statusPrefix", status);
+ }
+
+ return addHref(
+ uriInfo,
+ listInternal(uriInfo, securityContext, fieldsParam, filter, limitParam, before, after));
+ }
+
+ @GET
+ @Path("/{id}")
+ @Operation(
+ operationId = "getLearningResource",
+ summary = "Get a learning resource by id",
+ description = "Get a learning resource by `id`.",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "The learning resource",
+ content =
+ @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = LearningResource.class))),
+ @ApiResponse(responseCode = "404", description = "Resource not found")
+ })
+ public LearningResource get(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Fields requested in the returned resource") @QueryParam("fields")
+ String fieldsParam,
+ @Parameter(description = "Include all, deleted, or non-deleted entities")
+ @QueryParam("include")
+ @DefaultValue("non-deleted")
+ Include include,
+ @Parameter(description = "Id of the learning resource", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return getInternal(uriInfo, securityContext, id, fieldsParam, include);
+ }
+
+ @GET
+ @Path("/name/{name}")
+ @Operation(
+ operationId = "getLearningResourceByName",
+ summary = "Get a learning resource by name",
+ description = "Get a learning resource by fully qualified name.",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "The learning resource",
+ content =
+ @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = LearningResource.class))),
+ @ApiResponse(responseCode = "404", description = "Resource not found")
+ })
+ public LearningResource getByName(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Fully qualified name of the learning resource") @PathParam("name")
+ String name,
+ @Parameter(description = "Fields requested in the returned resource") @QueryParam("fields")
+ String fieldsParam,
+ @Parameter(description = "Include deleted resources")
+ @QueryParam("include")
+ @DefaultValue("non-deleted")
+ Include include) {
+ return getByNameInternal(uriInfo, securityContext, name, fieldsParam, include);
+ }
+
+ @GET
+ @Path("/{id}/versions")
+ @Operation(
+ summary = "List learning resource versions",
+ description = "Get a list of versions for the specified learning resource.",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "List of versions",
+ content =
+ @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = EntityHistory.class)))
+ })
+ public EntityHistory listVersions(
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the learning resource", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return listVersionsInternal(securityContext, id);
+ }
+
+ @GET
+ @Path("/{id}/versions/{version}")
+ @Operation(
+ summary = "Get a learning resource version",
+ description = "Get a specific version of the learning resource.",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Learning resource version details",
+ content =
+ @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = LearningResource.class)))
+ })
+ public LearningResource getVersion(
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the learning resource", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id,
+ @Parameter(description = "Learning resource version", schema = @Schema(type = "string"))
+ @PathParam("version")
+ String version) {
+ return getVersionInternal(securityContext, id, version);
+ }
+
+ @POST
+ @Operation(
+ operationId = "createLearningResource",
+ summary = "Create a learning resource",
+ description = "Create a new learning resource entry.",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "The created resource",
+ content =
+ @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = LearningResource.class)))
+ })
+ public Response create(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Valid CreateLearningResource create) {
+ LearningResource resource = toEntity(create, securityContext.getUserPrincipal().getName());
+ return create(uriInfo, securityContext, resource);
+ }
+
+ @PUT
+ @Operation(
+ operationId = "createOrUpdateLearningResource",
+ summary = "Create or update a learning resource",
+ description =
+ "Create a new learning resource, or update an existing one if it already exists.",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "The updated resource",
+ content =
+ @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = LearningResource.class)))
+ })
+ public Response createOrUpdate(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Valid CreateLearningResource create) {
+ LearningResource resource = toEntity(create, securityContext.getUserPrincipal().getName());
+ return createOrUpdate(uriInfo, securityContext, resource);
+ }
+
+ @PATCH
+ @Path("/{id}")
+ @Consumes(MediaType.APPLICATION_JSON_PATCH_JSON)
+ @Operation(
+ operationId = "patchLearningResource",
+ summary = "Update a learning resource",
+ description = "Apply a JSONPatch to a learning resource.",
+ externalDocs =
+ @ExternalDocumentation(
+ description = "JsonPatch RFC",
+ url = "https://tools.ietf.org/html/rfc6902"))
+ public Response patch(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the learning resource", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id,
+ @RequestBody(
+ description = "JsonPatch with array of operations",
+ content =
+ @Content(
+ mediaType = MediaType.APPLICATION_JSON_PATCH_JSON,
+ examples =
+ @ExampleObject("[{op:replace, path:/displayName, value: 'New name'}]")))
+ JsonPatch patch) {
+ return patchInternal(uriInfo, securityContext, id, patch);
+ }
+
+ @DELETE
+ @Path("/{id}")
+ @Operation(
+ operationId = "deleteLearningResource",
+ summary = "Delete a learning resource",
+ description = "Delete a learning resource by `id`.",
+ responses = {
+ @ApiResponse(responseCode = "200", description = "OK"),
+ @ApiResponse(responseCode = "404", description = "Resource not found")
+ })
+ public Response delete(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Recursively delete this entity and its children. (Default = false)")
+ @DefaultValue("false")
+ @QueryParam("recursive")
+ boolean recursive,
+ @Parameter(description = "Hard delete the entity. (Default = false)")
+ @DefaultValue("false")
+ @QueryParam("hardDelete")
+ boolean hardDelete,
+ @Parameter(description = "Id of the learning resource", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return delete(uriInfo, securityContext, id, recursive, hardDelete);
+ }
+
+ @PUT
+ @Path("/restore")
+ @Operation(
+ operationId = "restoreLearningResource",
+ summary = "Restore a soft-deleted learning resource",
+ description = "Restore a previously soft-deleted learning resource.",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "The restored resource",
+ content =
+ @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = LearningResource.class)))
+ })
+ public Response restore(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @RequestBody(
+ description = "Id of the learning resource to restore",
+ content =
+ @Content(
+ mediaType = "application/json",
+ schema = @Schema(type = "string", format = "uuid")))
+ RestoreEntity restore) {
+ return restoreEntity(uriInfo, securityContext, restore.getId());
+ }
+
+ private LearningResource toEntity(CreateLearningResource create, String updatedBy) {
+ LearningResource resource = repository.copy(new LearningResource(), create, updatedBy);
+ resource.setResourceType(create.getResourceType());
+ resource.setCategories(create.getCategories());
+ resource.setDifficulty(create.getDifficulty());
+ resource.setSource(create.getSource());
+ resource.setEstimatedDuration(create.getEstimatedDuration());
+ resource.setContexts(create.getContexts());
+ resource.setStatus(
+ create.getStatus() == null
+ ? null
+ : LearningResource.Status.fromValue(create.getStatus().value()));
+ return resource;
+ }
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_Automations.json b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_Automations.json
new file mode 100644
index 000000000000..724a20cf8b6b
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_Automations.json
@@ -0,0 +1,15 @@
+{
+ "name": "CollateClues_Automations",
+ "displayName": "Collate Clues: Automations",
+ "description": "Discover how to automate data governance workflows and tasks in Collate.",
+ "resourceType": "Video",
+ "categories": ["DataGovernance", "Administration"],
+ "source": {
+ "url": "https://www.youtube.com/watch?v=dJZ8wGAM-ek",
+ "provider": "YouTube"
+ },
+ "contexts": [
+ {"pageId": "automations"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_DataContracts.json b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_DataContracts.json
new file mode 100644
index 000000000000..3c6883e77728
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_DataContracts.json
@@ -0,0 +1,20 @@
+{
+ "name": "CollateClues_DataContracts",
+ "displayName": "Collate Clues: Data Contracts",
+ "description": "Learn how Data Contracts help align data consumers and producers, ensuring data quality and reliability across your organization.",
+ "resourceType": "Video",
+ "categories": ["DataGovernance", "DataQuality"],
+ "source": {
+ "url": "https://www.youtube.com/watch?v=xtQ_7IOpW7c",
+ "provider": "YouTube"
+ },
+ "contexts": [
+ {"pageId": "table"},
+ {"pageId": "topic"},
+ {"pageId": "dashboard"},
+ {"pageId": "pipeline"},
+ {"pageId": "mlmodel"},
+ {"pageId": "container"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_DataObservability.json b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_DataObservability.json
new file mode 100644
index 000000000000..398b3a0bde9b
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_DataObservability.json
@@ -0,0 +1,16 @@
+{
+ "name": "CollateClues_DataObservability",
+ "displayName": "Collate Clues: Data Observability",
+ "description": "Learn how Data Observability helps you monitor and ensure the health of your data pipelines and assets.",
+ "resourceType": "Video",
+ "categories": ["Observability", "DataQuality"],
+ "source": {
+ "url": "https://www.youtube.com/watch?v=ZOe71H1EuQ8",
+ "provider": "YouTube"
+ },
+ "contexts": [
+ {"pageId": "dataObservability"},
+ {"pageId": "table", "componentId": "profiler"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_DataProducts.json b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_DataProducts.json
new file mode 100644
index 000000000000..75b4e336eeae
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_DataProducts.json
@@ -0,0 +1,16 @@
+{
+ "name": "CollateClues_DataProducts",
+ "displayName": "Collate Clues: Data Products",
+ "description": "Quick overview of Data Products in Collate - learn how to organize and manage your data assets as products.",
+ "resourceType": "Video",
+ "categories": ["DataGovernance", "Discovery"],
+ "source": {
+ "url": "https://www.youtube.com/watch?v=-P7_qO1qYY4",
+ "provider": "YouTube"
+ },
+ "contexts": [
+ {"pageId": "dataProduct"},
+ {"pageId": "domain"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_DataQualityAdvanced.json b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_DataQualityAdvanced.json
new file mode 100644
index 000000000000..10345de6ee6f
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_DataQualityAdvanced.json
@@ -0,0 +1,16 @@
+{
+ "name": "CollateClues_DataQualityAdvanced",
+ "displayName": "Collate Clues: Advanced Data Quality",
+ "description": "Dive deeper into Data Quality features and learn advanced testing and monitoring capabilities.",
+ "resourceType": "Video",
+ "categories": ["DataQuality", "Observability"],
+ "source": {
+ "url": "https://www.youtube.com/watch?v=jIrQ-CsLEgg",
+ "provider": "YouTube"
+ },
+ "contexts": [
+ {"pageId": "dataObservability"},
+ {"pageId": "dataQuality"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_DataQualityBasics.json b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_DataQualityBasics.json
new file mode 100644
index 000000000000..777f9e9ad015
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_DataQualityBasics.json
@@ -0,0 +1,16 @@
+{
+ "name": "CollateClues_DataQualityBasics",
+ "displayName": "Collate Clues: Data Quality Basics",
+ "description": "Get started with Data Quality in Collate - learn the fundamentals of setting up quality tests and monitoring.",
+ "resourceType": "Video",
+ "categories": ["DataQuality", "Observability"],
+ "source": {
+ "url": "https://www.youtube.com/watch?v=HypaInv79PQ",
+ "provider": "YouTube"
+ },
+ "contexts": [
+ {"pageId": "dataObservability"},
+ {"pageId": "dataQuality"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_GettingStarted.json b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_GettingStarted.json
new file mode 100644
index 000000000000..fc222ac34ecb
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_GettingStarted.json
@@ -0,0 +1,15 @@
+{
+ "name": "CollateClues_GettingStarted",
+ "displayName": "Collate Clues: Getting Started",
+ "description": "Quick introduction to getting started with Collate - your first steps to unified metadata management.",
+ "resourceType": "Video",
+ "categories": ["Discovery", "Administration"],
+ "source": {
+ "url": "https://www.youtube.com/watch?v=DqIT4vWALGk",
+ "provider": "YouTube"
+ },
+ "contexts": [
+ {"pageId": "home"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_GovernanceWorkflows.json b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_GovernanceWorkflows.json
new file mode 100644
index 000000000000..db46ea1d7342
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_GovernanceWorkflows.json
@@ -0,0 +1,17 @@
+{
+ "name": "CollateClues_GovernanceWorkflows",
+ "displayName": "Collate Clues: Governance Workflows",
+ "description": "Learn how Governance Workflows help automate and enforce data governance policies across your organization.",
+ "resourceType": "Video",
+ "categories": ["DataGovernance"],
+ "source": {
+ "url": "https://www.youtube.com/watch?v=d6GEwZ-FGsw",
+ "provider": "YouTube"
+ },
+ "contexts": [
+ {"pageId": "governanceWorkflows"},
+ {"pageId": "domain"},
+ {"pageId": "dataProduct"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_Lineage.json b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_Lineage.json
new file mode 100644
index 000000000000..426af88fb2a8
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_Lineage.json
@@ -0,0 +1,15 @@
+{
+ "name": "CollateClues_Lineage",
+ "displayName": "Collate Clues: Lineage",
+ "description": "Understand data lineage visualization and how to trace data flow across your organization.",
+ "resourceType": "Video",
+ "categories": ["DataGovernance", "Discovery"],
+ "source": {
+ "url": "https://www.youtube.com/watch?v=21mk-I1H5Xo",
+ "provider": "YouTube"
+ },
+ "contexts": [
+ {"pageId": "lineage"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_ReverseMetadata.json b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_ReverseMetadata.json
new file mode 100644
index 000000000000..e05fe0b3c1ea
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_ReverseMetadata.json
@@ -0,0 +1,16 @@
+{
+ "name": "CollateClues_ReverseMetadata",
+ "displayName": "Collate Clues: Reverse Metadata",
+ "description": "Discover how Reverse Metadata pushes enriched metadata back to your data sources, keeping everything in sync.",
+ "resourceType": "Video",
+ "categories": ["Administration", "Discovery"],
+ "source": {
+ "url": "https://www.youtube.com/watch?v=uXnI-rKu12A",
+ "provider": "YouTube"
+ },
+ "contexts": [
+ {"pageId": "table"},
+ {"pageId": "explore"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_RolesAndPolicies.json b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_RolesAndPolicies.json
new file mode 100644
index 000000000000..1b7d46630adc
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_RolesAndPolicies.json
@@ -0,0 +1,17 @@
+{
+ "name": "CollateClues_RolesAndPolicies",
+ "displayName": "Collate Clues: Roles & Policies",
+ "description": "Learn how to configure access control with Roles and Policies to secure your data assets.",
+ "resourceType": "Video",
+ "categories": ["Administration", "DataGovernance"],
+ "source": {
+ "url": "https://www.youtube.com/watch?v=Qy28FaNgkgM",
+ "provider": "YouTube"
+ },
+ "contexts": [
+ {"pageId": "roles"},
+ {"pageId": "policies"},
+ {"pageId": "accessControl"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_Services.json b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_Services.json
new file mode 100644
index 000000000000..6e5d387316b2
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/CollateClues_Services.json
@@ -0,0 +1,21 @@
+{
+ "name": "CollateClues_Services",
+ "displayName": "Collate Clues: Services Setup",
+ "description": "Quick guide to setting up and configuring services in Collate to connect your data sources.",
+ "resourceType": "Video",
+ "categories": ["Administration"],
+ "source": {
+ "url": "https://www.youtube.com/watch?v=SCqAnJeViCE",
+ "provider": "YouTube"
+ },
+ "contexts": [
+ {"pageId": "services"},
+ {"pageId": "databaseServices"},
+ {"pageId": "messagingServices"},
+ {"pageId": "dashboardServices"},
+ {"pageId": "pipelineServices"},
+ {"pageId": "mlmodelServices"},
+ {"pageId": "storageServices"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/Demo_ConnectorAgents.json b/openmetadata-service/src/main/resources/json/data/learningResource/Demo_ConnectorAgents.json
new file mode 100644
index 000000000000..2caea0bcdfd1
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/Demo_ConnectorAgents.json
@@ -0,0 +1,18 @@
+{
+ "name": "Demo_ConnectorAgents",
+ "displayName": "Connector Agents Overview",
+ "description": "Learn about the variety of connector agents that Collate uses to automate and simplify your work. Get an overview of Metadata Agents and AI Agents designed to help teams automate workflows.",
+ "resourceType": "Storylane",
+ "categories": ["Discovery", "Administration"],
+ "difficulty": "Intro",
+ "source": {
+ "url": "https://collate.storylane.io/demo/yquttvft7hxj",
+ "provider": "Collate"
+ },
+ "estimatedDuration": 300,
+ "contexts": [
+ {"pageId": "explore"},
+ {"pageId": "pipeline"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/Demo_DataContracts.json b/openmetadata-service/src/main/resources/json/data/learningResource/Demo_DataContracts.json
new file mode 100644
index 000000000000..1c7541b3683c
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/Demo_DataContracts.json
@@ -0,0 +1,19 @@
+{
+ "name": "Demo_DataContracts",
+ "displayName": "Data Contracts",
+ "description": "Learn how Data Contracts help align consumers and producers of data. This demo covers creating contracts, understanding contract segments, and reviewing completion records with run history.",
+ "resourceType": "Storylane",
+ "categories": ["DataGovernance", "DataQuality"],
+ "difficulty": "Intermediate",
+ "source": {
+ "url": "https://collate.storylane.io/demo/mhr2lbg0jse6",
+ "provider": "Collate"
+ },
+ "estimatedDuration": 300,
+ "contexts": [
+ {"pageId": "table"},
+ {"pageId": "dataQuality"},
+ {"pageId": "glossary"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/Demo_DataQualityBasics.json b/openmetadata-service/src/main/resources/json/data/learningResource/Demo_DataQualityBasics.json
new file mode 100644
index 000000000000..9637f727457b
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/Demo_DataQualityBasics.json
@@ -0,0 +1,18 @@
+{
+ "name": "Demo_DataQualityBasics",
+ "displayName": "Data Quality Basics",
+ "description": "Learn how Collate's data observability catches quality issues before they impact your business. This demo covers establishing data quality tests, constructing pipelines, and configuring schedules to evaluate data freshness metrics.",
+ "resourceType": "Storylane",
+ "categories": ["DataQuality", "Observability"],
+ "difficulty": "Intro",
+ "source": {
+ "url": "https://collate.storylane.io/demo/c8dmf6yddhkn",
+ "provider": "Collate"
+ },
+ "estimatedDuration": 300,
+ "contexts": [
+ {"pageId": "table"},
+ {"pageId": "dataQuality"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/Demo_GettingStarted.json b/openmetadata-service/src/main/resources/json/data/learningResource/Demo_GettingStarted.json
new file mode 100644
index 000000000000..94f0d45ded17
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/Demo_GettingStarted.json
@@ -0,0 +1,17 @@
+{
+ "name": "Demo_GettingStarted",
+ "displayName": "Getting Started with Collate",
+ "description": "Learn how to get your Collate instance set up in just a couple minutes. This demo covers setting up a Snowflake connector, using AI agents for automatic documentation, and viewing entity relationship diagrams.",
+ "resourceType": "Storylane",
+ "categories": ["Discovery", "Administration"],
+ "difficulty": "Intro",
+ "source": {
+ "url": "https://collate.storylane.io/demo/dlumjorwxy9m",
+ "provider": "Collate"
+ },
+ "estimatedDuration": 300,
+ "contexts": [
+ {"pageId": "explore"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/Demo_LineageBasics.json b/openmetadata-service/src/main/resources/json/data/learningResource/Demo_LineageBasics.json
new file mode 100644
index 000000000000..4645d896c485
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/Demo_LineageBasics.json
@@ -0,0 +1,20 @@
+{
+ "name": "Demo_LineageBasics",
+ "displayName": "Lineage Basics",
+ "description": "Discover how Collate streamlines data governance by providing robust tracing of your data lineage. Learn foundational concepts, configure the Lineage Agent, and navigate table and column-level data lineage visibility.",
+ "resourceType": "Storylane",
+ "categories": ["DataGovernance", "Discovery"],
+ "difficulty": "Intro",
+ "source": {
+ "url": "https://collate.storylane.io/demo/vincstg0kjcg",
+ "provider": "Collate"
+ },
+ "estimatedDuration": 300,
+ "contexts": [
+ {"pageId": "table"},
+ {"pageId": "pipeline"},
+ {"pageId": "dashboard"},
+ {"pageId": "glossary"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/Video_BigQueryIntegration.json b/openmetadata-service/src/main/resources/json/data/learningResource/Video_BigQueryIntegration.json
new file mode 100644
index 000000000000..753bdfcb2478
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/Video_BigQueryIntegration.json
@@ -0,0 +1,18 @@
+{
+ "name": "Video_BigQueryIntegration",
+ "displayName": "Connecting BigQuery to Collate",
+ "description": "Learn how to connect your BigQuery database to Collate's AI-powered platform. Unlock Google's data ecosystem with seamless integration.",
+ "resourceType": "Video",
+ "categories": ["Discovery", "Administration"],
+ "difficulty": "Intermediate",
+ "source": {
+ "url": "https://www.youtube.com/watch?v=aOaWniA9K40",
+ "provider": "YouTube"
+ },
+ "estimatedDuration": 600,
+ "contexts": [
+ {"pageId": "table"},
+ {"pageId": "explore"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/main/resources/json/data/learningResource/Video_SnowflakeIntegration.json b/openmetadata-service/src/main/resources/json/data/learningResource/Video_SnowflakeIntegration.json
new file mode 100644
index 000000000000..2e6279a5e3e5
--- /dev/null
+++ b/openmetadata-service/src/main/resources/json/data/learningResource/Video_SnowflakeIntegration.json
@@ -0,0 +1,18 @@
+{
+ "name": "Video_SnowflakeIntegration",
+ "displayName": "Connecting Snowflake to Collate",
+ "description": "Learn how to connect your Snowflake database to Collate's AI-powered platform. This video covers reverse metadata capability and usage analytics including query execution times and costs.",
+ "resourceType": "Video",
+ "categories": ["Discovery", "Administration"],
+ "difficulty": "Intermediate",
+ "source": {
+ "url": "https://www.youtube.com/watch?v=EfycmnM_hPs",
+ "provider": "YouTube"
+ },
+ "estimatedDuration": 600,
+ "contexts": [
+ {"pageId": "table"},
+ {"pageId": "explore"}
+ ],
+ "status": "Active"
+}
diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java
index 5f7301727200..7c6da3d8aa00 100644
--- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java
+++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java
@@ -329,7 +329,7 @@ public abstract class EntityResourceTest {
+ test.beforeEach(async ({ page }) => {
+ await redirectToHomePage(page);
+ await settingClick(page, GlobalSettingOptions.LEARNING_RESOURCES);
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
+ // Wait for the table to be fully loaded
+ await page.waitForSelector('.ant-table-tbody');
+ });
+
+ test('should display learning resources page', async ({ page }) => {
+ await expect(page.getByTestId('learning-resources-page')).toBeVisible();
+ await expect(page.getByTestId('page-title')).toContainText('Learning Resource');
+ await expect(page.getByTestId('create-resource')).toBeVisible();
+ });
+
+ test('should open and close add resource drawer', async ({ page }) => {
+ await test.step('Open add resource drawer', async () => {
+ await page.getByTestId('create-resource').click();
+ await expect(page.locator('.drawer-title')).toContainText('Add Resource');
+ });
+
+ await test.step('Close drawer', async () => {
+ await page.locator('.drawer-close').click();
+ await expect(page.locator('.drawer-title')).not.toBeVisible();
+ });
+ });
+
+ test('should validate required fields', async ({ page }) => {
+ await page.getByTestId('create-resource').click();
+ await expect(page.locator('.drawer-title')).toBeVisible();
+
+ // Try to submit without filling required fields
+ await page.getByTestId('save-resource').click();
+
+ // Expect validation errors to appear
+ await expect(page.locator('.ant-form-item-explain-error').first()).toBeVisible();
+
+ // Close drawer
+ await page.locator('.drawer-close').click();
+ });
+
+ test('should edit an existing learning resource', async ({ page }) => {
+ const { apiContext, afterAction } = await getApiContext(page);
+ const uniqueId = uuid();
+ const resource = new LearningResourceClass({
+ name: `PW_Edit_Resource_${uniqueId}`,
+ displayName: `PW Edit Resource ${uniqueId}`,
+ description: 'Resource to be edited',
+ });
+
+ await resource.create(apiContext);
+
+ // Reload to get fresh data after creating resource
+ await page.reload();
+ await page.waitForSelector('.ant-table-tbody');
+
+ // Search for the resource to find it
+ await searchResource(page, uniqueId);
+ await expect(page.getByText(resource.data.displayName ?? '')).toBeVisible({ timeout: 10000 });
+
+ await test.step('Click edit button and verify drawer opens', async () => {
+ await page.getByTestId(`edit-${resource.data.name}`).click();
+ await expect(page.locator('.drawer-title')).toContainText('Edit Resource');
+ // Verify the form is populated with resource data
+ await expect(page.locator('#name')).toHaveValue(resource.data.name);
+ });
+
+ await test.step('Close the drawer', async () => {
+ await page.locator('.drawer-close').click();
+ await expect(page.locator('.drawer-title')).not.toBeVisible();
+ });
+
+ await resource.delete(apiContext);
+ await afterAction();
+ });
+
+ test('should delete a learning resource', async ({ page }) => {
+ const { apiContext, afterAction } = await getApiContext(page);
+ const uniqueId = uuid();
+ const resource = new LearningResourceClass({
+ name: `PW_Delete_Resource_${uniqueId}`,
+ displayName: `PW Delete Resource ${uniqueId}`,
+ });
+
+ await resource.create(apiContext);
+
+ // Reload to get fresh data after creating resource
+ await page.reload();
+ await page.waitForSelector('.ant-table-tbody');
+
+ // Search for the resource to find it
+ await searchResource(page, uniqueId);
+ await expect(page.getByText(resource.data.displayName ?? '')).toBeVisible({ timeout: 10000 });
+
+ await test.step('Click delete button and confirm', async () => {
+ await page.getByTestId(`delete-${resource.data.name}`).click();
+ // Wait for the confirmation modal to appear
+ await expect(page.locator('.ant-modal-confirm')).toBeVisible({ timeout: 5000 });
+ // Click the OK/Delete button in the modal
+ await page.locator('.ant-modal-confirm-btns button').filter({ hasText: /delete|ok/i }).click();
+ });
+
+ await test.step('Verify resource is removed from list', async () => {
+ // Wait for modal to close and table to update
+ await expect(page.locator('.ant-modal-confirm')).not.toBeVisible({ timeout: 5000 });
+ await page.waitForTimeout(500);
+ await expect(page.getByText(resource.data.displayName ?? '')).not.toBeVisible();
+ });
+
+ await afterAction();
+ });
+
+ test('should preview a learning resource by clicking on name', async ({ page }) => {
+ const { apiContext, afterAction } = await getApiContext(page);
+ const uniqueId = uuid();
+ const resource = new LearningResourceClass({
+ name: `PW_Preview_Resource_${uniqueId}`,
+ displayName: `PW Preview Resource ${uniqueId}`,
+ source: {
+ url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
+ provider: 'YouTube',
+ },
+ });
+
+ await resource.create(apiContext);
+
+ // Reload to get fresh data after creating resource
+ await page.reload();
+ await page.waitForSelector('.ant-table-tbody');
+
+ // Search for the resource to find it
+ await searchResource(page, uniqueId);
+ await expect(page.getByText(resource.data.displayName ?? '')).toBeVisible({ timeout: 10000 });
+
+ await test.step('Click on resource name to preview', async () => {
+ await page.getByText(resource.data.displayName ?? '').click();
+ });
+
+ await test.step('Verify preview modal opens', async () => {
+ await expect(page.locator('.ant-modal')).toBeVisible();
+ });
+
+ await test.step('Close preview modal', async () => {
+ // Close button is in the modal header with class 'close-button'
+ await page.locator('.close-button').click();
+ await expect(page.locator('.ant-modal')).not.toBeVisible({ timeout: 5000 });
+ });
+
+ await resource.delete(apiContext);
+ await afterAction();
+ });
+
+ test('should filter resources by type', async ({ page }) => {
+ const { apiContext, afterAction } = await getApiContext(page);
+ const uniqueId = uuid();
+ const videoResource = new LearningResourceClass({
+ name: `PW_Video_Resource_${uniqueId}`,
+ displayName: `PW Video Resource ${uniqueId}`,
+ resourceType: 'Video',
+ });
+
+ await videoResource.create(apiContext);
+
+ // Reload to get fresh data
+ await page.reload();
+ await page.waitForSelector('.ant-table-tbody');
+
+ await test.step('Filter by Video type', async () => {
+ await page.locator('.filter-select').filter({ hasText: 'Type' }).click();
+ await selectDropdownOption(page, 'Video');
+
+ // Search for our specific resource
+ await searchResource(page, uniqueId);
+ await expect(page.getByText(`PW Video Resource ${uniqueId}`)).toBeVisible({ timeout: 10000 });
+ });
+
+ await videoResource.delete(apiContext);
+ await afterAction();
+ });
+
+ test('should search resources by name', async ({ page }) => {
+ const { apiContext, afterAction } = await getApiContext(page);
+ const uniqueId = uuid();
+ const resource = new LearningResourceClass({
+ name: `PW_Search_Resource_${uniqueId}`,
+ displayName: `PW Search Resource ${uniqueId}`,
+ });
+
+ await resource.create(apiContext);
+
+ // Reload to get fresh data
+ await page.reload();
+ await page.waitForSelector('.ant-table-tbody');
+
+ await test.step('Search for resource', async () => {
+ await searchResource(page, uniqueId);
+ await expect(page.getByText(`PW Search Resource ${uniqueId}`)).toBeVisible({ timeout: 10000 });
+ });
+
+ await resource.delete(apiContext);
+ await afterAction();
+ });
+});
+
+test.describe('Learning Resources Admin Page - Additional Tests', () => {
+ test.beforeEach(async ({ page }) => {
+ await redirectToHomePage(page);
+ await settingClick(page, GlobalSettingOptions.LEARNING_RESOURCES);
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
+ await page.waitForSelector('.ant-table-tbody');
+ });
+
+ test('should filter resources by category', async ({ page }) => {
+ const { apiContext, afterAction } = await getApiContext(page);
+ const uniqueId = uuid();
+ const resource = new LearningResourceClass({
+ name: `PW_Category_Resource_${uniqueId}`,
+ displayName: `PW Category Resource ${uniqueId}`,
+ categories: ['DataGovernance'],
+ });
+
+ await resource.create(apiContext);
+
+ await page.reload();
+ await page.waitForSelector('.ant-table-tbody');
+
+ await test.step('Filter by Governance category', async () => {
+ await page.locator('.filter-select').filter({ hasText: 'Category' }).click();
+ await selectDropdownOption(page, 'Governance');
+
+ await searchResource(page, uniqueId);
+ await expect(page.getByText(`PW Category Resource ${uniqueId}`)).toBeVisible({ timeout: 10000 });
+ });
+
+ await resource.delete(apiContext);
+ await afterAction();
+ });
+
+ test('should filter resources by status', async ({ page }) => {
+ const { apiContext, afterAction } = await getApiContext(page);
+ const uniqueId = uuid();
+ const resource = new LearningResourceClass({
+ name: `PW_Status_Resource_${uniqueId}`,
+ displayName: `PW Status Resource ${uniqueId}`,
+ status: 'Draft',
+ });
+
+ await resource.create(apiContext);
+
+ await page.reload();
+ await page.waitForSelector('.ant-table-tbody');
+
+ await test.step('Filter by Draft status', async () => {
+ await page.locator('.filter-select').filter({ hasText: 'Status' }).click();
+ await selectDropdownOption(page, 'Draft');
+
+ await searchResource(page, uniqueId);
+ await expect(page.getByText(`PW Status Resource ${uniqueId}`)).toBeVisible({ timeout: 10000 });
+ });
+
+ await resource.delete(apiContext);
+ await afterAction();
+ });
+
+ test('should edit and save resource changes via UI', async ({ page }) => {
+ const { apiContext, afterAction } = await getApiContext(page);
+ const uniqueId = uuid();
+ const resource = new LearningResourceClass({
+ name: `PW_Edit_Save_Resource_${uniqueId}`,
+ displayName: `PW Edit Save Resource ${uniqueId}`,
+ description: 'Original description',
+ });
+
+ await resource.create(apiContext);
+
+ await page.reload();
+ await page.waitForSelector('.ant-table-tbody');
+
+ await searchResource(page, uniqueId);
+ await expect(page.getByText(resource.data.displayName ?? '')).toBeVisible({ timeout: 10000 });
+
+ await test.step('Open edit drawer and modify display name', async () => {
+ await page.getByTestId(`edit-${resource.data.name}`).click();
+ await expect(page.locator('.drawer-title')).toContainText('Edit Resource');
+
+ // Clear and update display name
+ await page.locator('#displayName').clear();
+ await page.locator('#displayName').fill(`Updated Resource ${uniqueId}`);
+ });
+
+ await test.step('Save changes', async () => {
+ await page.getByTestId('save-resource').click();
+ await expect(page.locator('.drawer-title')).not.toBeVisible({ timeout: 10000 });
+ });
+
+ await test.step('Verify updated display name in list', async () => {
+ await page.reload();
+ await page.waitForSelector('.ant-table-tbody');
+ await searchResource(page, uniqueId);
+ await expect(page.getByText(`Updated Resource ${uniqueId}`)).toBeVisible({ timeout: 10000 });
+ });
+
+ await resource.delete(apiContext);
+ await afterAction();
+ });
+});
+
+test.describe('Learning Icon on Pages', () => {
+ test('should display learning icon on glossary page when resources exist', async ({ page }) => {
+ // Navigate to home first to ensure auth context is established
+ await redirectToHomePage(page);
+
+ const { apiContext, afterAction } = await getApiContext(page);
+ const resource = new LearningResourceClass({
+ name: `PW_Glossary_Icon_Resource_${uuid()}`,
+ displayName: `PW Glossary Icon Resource`,
+ contexts: [{ pageId: 'glossary' }],
+ status: 'Active',
+ });
+
+ await resource.create(apiContext);
+
+ await page.goto('/glossary');
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
+
+ const learningIcon = page.locator('[data-testid="learning-icon"]');
+ await expect(learningIcon).toBeVisible({ timeout: 10000 });
+
+ await resource.delete(apiContext);
+ await afterAction();
+ });
+
+ test('should open learning drawer when icon is clicked', async ({ page }) => {
+ // Navigate to home first to ensure auth context is established
+ await redirectToHomePage(page);
+
+ const { apiContext, afterAction } = await getApiContext(page);
+ const resource = new LearningResourceClass({
+ name: `PW_Glossary_Drawer_Resource_${uuid()}`,
+ displayName: `PW Glossary Drawer Resource`,
+ contexts: [{ pageId: 'glossary' }],
+ status: 'Active',
+ });
+
+ await resource.create(apiContext);
+
+ await page.goto('/glossary');
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
+
+ await test.step('Click learning icon', async () => {
+ const learningIcon = page.locator('[data-testid="learning-icon"]');
+ await expect(learningIcon).toBeVisible({ timeout: 10000 });
+ await learningIcon.click();
+ });
+
+ await test.step('Verify drawer opens with resources', async () => {
+ await expect(page.locator('.learning-drawer')).toBeVisible();
+ });
+
+ await test.step('Close drawer', async () => {
+ await page.keyboard.press('Escape');
+ });
+
+ await resource.delete(apiContext);
+ await afterAction();
+ });
+
+ test('should NOT show draft resources on target pages', async ({ page }) => {
+ // Navigate to home first to ensure auth context is established
+ await redirectToHomePage(page);
+
+ const { apiContext, afterAction } = await getApiContext(page);
+ const uniqueId = uuid();
+ const draftResource = new LearningResourceClass({
+ name: `PW_Draft_Resource_${uniqueId}`,
+ displayName: `PW Draft Resource ${uniqueId}`,
+ contexts: [{ pageId: 'glossary' }],
+ status: 'Draft', // Draft status should NOT appear on pages
+ });
+
+ await draftResource.create(apiContext);
+
+ await page.goto('/glossary');
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
+ await page.waitForTimeout(2000);
+
+ // Check if learning icon exists
+ const learningIcon = page.locator('[data-testid="learning-icon"]');
+ const isIconVisible = await learningIcon.isVisible().catch(() => false);
+
+ if (isIconVisible) {
+ // If icon is visible, our draft resource should NOT be in the drawer
+ await learningIcon.click();
+ await expect(page.locator('.learning-drawer')).toBeVisible();
+ await expect(page.getByText(`PW Draft Resource ${uniqueId}`)).not.toBeVisible();
+ await page.keyboard.press('Escape');
+ }
+ // If icon is not visible, that's also valid (no active resources)
+
+ await draftResource.delete(apiContext);
+ await afterAction();
+ });
+
+ test('should show learning icon on lineage page when resources exist', async ({ page }) => {
+ // Navigate to home first to ensure auth context is established
+ await redirectToHomePage(page);
+
+ const { apiContext, afterAction } = await getApiContext(page);
+ const resource = new LearningResourceClass({
+ name: `PW_Lineage_Resource_${uuid()}`,
+ displayName: `PW Lineage Resource`,
+ contexts: [{ pageId: 'lineage' }],
+ status: 'Active',
+ });
+
+ await resource.create(apiContext);
+
+ await page.goto('/lineage');
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
+
+ const learningIcon = page.locator('[data-testid="learning-icon"]');
+ await expect(learningIcon).toBeVisible({ timeout: 15000 });
+
+ // Click and verify resource is shown
+ await learningIcon.click();
+ await expect(page.locator('.learning-drawer')).toBeVisible();
+ await expect(page.getByText('PW Lineage Resource')).toBeVisible();
+ await page.keyboard.press('Escape');
+
+ await resource.delete(apiContext);
+ await afterAction();
+ });
+
+ test('should open resource player when clicking on resource card in drawer', async ({ page }) => {
+ // Navigate to home first to ensure auth context is established
+ await redirectToHomePage(page);
+
+ const { apiContext, afterAction } = await getApiContext(page);
+ const uniqueId = uuid();
+ const resource = new LearningResourceClass({
+ name: `PW_Player_Resource_${uniqueId}`,
+ displayName: `PW Player Resource ${uniqueId}`,
+ contexts: [{ pageId: 'glossary' }],
+ status: 'Active',
+ source: {
+ url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
+ provider: 'YouTube',
+ },
+ });
+
+ await resource.create(apiContext);
+
+ await page.goto('/glossary');
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
+
+ await test.step('Open learning drawer', async () => {
+ const learningIcon = page.locator('[data-testid="learning-icon"]');
+ await expect(learningIcon).toBeVisible({ timeout: 15000 });
+ await learningIcon.click();
+ await expect(page.locator('.learning-drawer')).toBeVisible();
+ });
+
+ await test.step('Click on resource card to open player', async () => {
+ // Click on the resource card
+ await page.getByText(`PW Player Resource ${uniqueId}`).click();
+ // Verify player modal opens
+ await expect(page.locator('.ant-modal')).toBeVisible({ timeout: 10000 });
+ });
+
+ await test.step('Close player modal', async () => {
+ await page.locator('.close-button').click();
+ await expect(page.locator('.ant-modal')).not.toBeVisible({ timeout: 5000 });
+ });
+
+ await resource.delete(apiContext);
+ await afterAction();
+ });
+});
+
+test.describe.serial('Learning Resources E2E Flow', () => {
+ test('should create resource via UI and verify learning icon appears on target page', async ({ page }) => {
+ const uniqueId = uuid();
+ const resourceName = `PW_Create_E2E_${uniqueId}`;
+
+ await test.step('Navigate to Learning Resources admin page', async () => {
+ await redirectToHomePage(page);
+ await settingClick(page, GlobalSettingOptions.LEARNING_RESOURCES);
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
+ await page.waitForSelector('.ant-table-tbody', { timeout: 30000 });
+ });
+
+ await test.step('Open add resource drawer and fill form', async () => {
+ await page.getByTestId('create-resource').click();
+ await expect(page.locator('.drawer-title')).toContainText('Add Resource');
+
+ // Fill required fields
+ await page.locator('#name').fill(resourceName);
+ await page.locator('#displayName').fill(`E2E Test Resource ${uniqueId}`);
+ await page.locator('textarea#description').fill('E2E test learning resource');
+
+ // Select type
+ await page.getByTestId('resource-type-form-item').locator('.ant-select-selector').click();
+ await selectDropdownOption(page, 'Video');
+
+ // Select category
+ await page.getByTestId('categories-form-item').locator('.ant-select-selector').click();
+ await selectDropdownOption(page, 'Discovery');
+ await page.keyboard.press('Escape');
+
+ // Select context - Glossary page
+ await page.getByTestId('contexts-form-item').locator('.ant-select-selector').click();
+ await selectDropdownOption(page, 'Glossary');
+ await page.keyboard.press('Escape');
+
+ // Fill source URL
+ await page.locator('#sourceUrl').fill('https://www.youtube.com/watch?v=test123');
+
+ // Set status to Active
+ await page.locator('.ant-form-item').filter({ hasText: 'Status' }).locator('.ant-select-selector').click();
+ await selectDropdownOption(page, 'Active');
+ });
+
+ await test.step('Save the resource', async () => {
+ await page.getByTestId('save-resource').click();
+ // Wait for drawer to close indicating success
+ await expect(page.locator('.drawer-title')).not.toBeVisible({ timeout: 10000 });
+ });
+
+ await test.step('Navigate to Glossary page and verify learning icon appears', async () => {
+ await page.goto('/glossary');
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
+
+ const learningIcon = page.locator('[data-testid="learning-icon"]');
+ await expect(learningIcon).toBeVisible({ timeout: 15000 });
+ });
+
+ await test.step('Click learning icon and verify the created resource is shown', async () => {
+ await page.locator('[data-testid="learning-icon"]').click();
+ await expect(page.locator('.learning-drawer')).toBeVisible();
+ await expect(page.getByText(`E2E Test Resource ${uniqueId}`)).toBeVisible();
+ await page.keyboard.press('Escape');
+ });
+
+ await test.step('Cleanup - delete the created resource', async () => {
+ // Navigate back to Learning Resources admin page
+ await page.goto('/settings/preferences/learning-resources');
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached', timeout: 30000 });
+ await page.waitForSelector('.ant-table-tbody', { timeout: 30000 });
+
+ await searchResource(page, uniqueId);
+ await expect(page.getByText(`E2E Test Resource ${uniqueId}`)).toBeVisible({ timeout: 10000 });
+
+ await page.getByTestId(`delete-${resourceName}`).click();
+ await expect(page.locator('.ant-modal-confirm')).toBeVisible({ timeout: 5000 });
+ await page.locator('.ant-modal-confirm-btns button').filter({ hasText: /delete|ok/i }).click();
+ await expect(page.locator('.ant-modal-confirm')).not.toBeVisible({ timeout: 5000 });
+ });
+ });
+
+ test('should update resource context and verify learning icon moves to new page', async ({ page }) => {
+ // Navigate to home first to ensure auth context is established
+ await redirectToHomePage(page);
+
+ const { apiContext, afterAction } = await getApiContext(page);
+ const uniqueId = uuid();
+ const resource = new LearningResourceClass({
+ name: `PW_Update_Context_${uniqueId}`,
+ displayName: `Update Context Resource ${uniqueId}`,
+ contexts: [{ pageId: 'glossary' }],
+ status: 'Active',
+ });
+
+ const createdResource = await resource.create(apiContext);
+ expect(createdResource, `Failed to create resource: ${JSON.stringify(createdResource)}`).toBeDefined();
+ expect(createdResource.id, `Resource ID is undefined: ${JSON.stringify(createdResource)}`).toBeDefined();
+ expect(createdResource.displayName).toBe(`Update Context Resource ${uniqueId}`);
+
+ await test.step('Verify resource appears on Glossary page initially', async () => {
+ await page.goto('/glossary');
+ await page.waitForLoadState('networkidle');
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached', timeout: 30000 });
+
+ const learningIcon = page.locator('[data-testid="learning-icon"]');
+ await expect(learningIcon).toBeVisible({ timeout: 20000 });
+
+ // Verify our resource is in the drawer
+ await learningIcon.click();
+ await expect(page.locator('.learning-drawer')).toBeVisible();
+ await expect(page.getByText(`Update Context Resource ${uniqueId}`)).toBeVisible({ timeout: 10000 });
+ await page.keyboard.press('Escape');
+ });
+
+ await test.step('Navigate to admin page and update resource context to Lineage', async () => {
+ await page.goto('/settings/preferences/learning-resources');
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached', timeout: 30000 });
+ await page.waitForSelector('.ant-table-tbody', { timeout: 30000 });
+
+ await searchResource(page, uniqueId);
+ await expect(page.getByText(`Update Context Resource ${uniqueId}`)).toBeVisible({ timeout: 10000 });
+
+ // Click edit button
+ await page.getByTestId(`edit-${resource.data.name}`).click();
+ await expect(page.locator('.drawer-title')).toContainText('Edit Resource');
+
+ // Clear existing contexts and add new one - Lineage
+ await page.getByTestId('contexts-form-item').locator('.ant-select-selection-item-remove').click();
+ await page.getByTestId('contexts-form-item').locator('.ant-select-selector').click();
+ await selectDropdownOption(page, 'Lineage');
+ await page.keyboard.press('Escape');
+
+ // Save changes
+ await page.getByTestId('save-resource').click();
+ await expect(page.locator('.drawer-title')).not.toBeVisible({ timeout: 10000 });
+ });
+
+ await test.step('Verify learning icon no longer appears on Glossary page', async () => {
+ await page.goto('/glossary');
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
+ await page.waitForTimeout(2000); // Give time for API to respond
+
+ // The learning icon should not be visible or should not show our resource
+ const learningIcon = page.locator('[data-testid="learning-icon"]');
+ const isIconVisible = await learningIcon.isVisible().catch(() => false);
+
+ if (isIconVisible) {
+ // If icon is visible, our resource should not be in the drawer
+ await learningIcon.click();
+ await expect(page.locator('.learning-drawer')).toBeVisible();
+ await expect(page.getByText(`Update Context Resource ${uniqueId}`)).not.toBeVisible();
+ await page.keyboard.press('Escape');
+ }
+ });
+
+ await test.step('Verify learning icon now appears on Lineage page', async () => {
+ await page.goto('/lineage');
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
+
+ const learningIcon = page.locator('[data-testid="learning-icon"]');
+ await expect(learningIcon).toBeVisible({ timeout: 15000 });
+
+ // Verify our resource is in the drawer
+ await learningIcon.click();
+ await expect(page.locator('.learning-drawer')).toBeVisible();
+ await expect(page.getByText(`Update Context Resource ${uniqueId}`)).toBeVisible();
+ await page.keyboard.press('Escape');
+ });
+
+ await resource.delete(apiContext);
+ await afterAction();
+ });
+
+ test('should delete resource and verify learning icon disappears from target page', async ({ page }) => {
+ // Navigate to home first to ensure auth context is established
+ await redirectToHomePage(page);
+
+ const { apiContext, afterAction } = await getApiContext(page);
+ const uniqueId = uuid();
+ const resource = new LearningResourceClass({
+ name: `PW_Delete_E2E_${uniqueId}`,
+ displayName: `Delete E2E Resource ${uniqueId}`,
+ contexts: [{ pageId: 'glossary' }],
+ status: 'Active',
+ });
+
+ const createdResource = await resource.create(apiContext);
+ expect(createdResource.id).toBeDefined();
+ expect(createdResource.displayName).toBe(`Delete E2E Resource ${uniqueId}`);
+
+ await test.step('Verify resource appears on Glossary page initially', async () => {
+ await page.goto('/glossary');
+ await page.waitForLoadState('networkidle');
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached', timeout: 30000 });
+
+ const learningIcon = page.locator('[data-testid="learning-icon"]');
+ await expect(learningIcon).toBeVisible({ timeout: 20000 });
+
+ // Verify our resource is in the drawer
+ await learningIcon.click();
+ await expect(page.locator('.learning-drawer')).toBeVisible();
+ await expect(page.getByText(`Delete E2E Resource ${uniqueId}`)).toBeVisible({ timeout: 10000 });
+ await page.keyboard.press('Escape');
+ });
+
+ await test.step('Navigate to admin page and delete the resource', async () => {
+ await page.goto('/settings/preferences/learning-resources');
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached', timeout: 30000 });
+ await page.waitForSelector('.ant-table-tbody', { timeout: 30000 });
+
+ await searchResource(page, uniqueId);
+ await expect(page.getByText(`Delete E2E Resource ${uniqueId}`)).toBeVisible({ timeout: 10000 });
+
+ await page.getByTestId(`delete-${resource.data.name}`).click();
+ await expect(page.locator('.ant-modal-confirm')).toBeVisible({ timeout: 5000 });
+ await page.locator('.ant-modal-confirm-btns button').filter({ hasText: /delete|ok/i }).click();
+ await expect(page.locator('.ant-modal-confirm')).not.toBeVisible({ timeout: 5000 });
+ });
+
+ await test.step('Verify learning icon no longer shows deleted resource on Glossary page', async () => {
+ await page.goto('/glossary');
+ await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
+ await page.waitForTimeout(2000); // Give time for API to respond
+
+ const learningIcon = page.locator('[data-testid="learning-icon"]');
+ const isIconVisible = await learningIcon.isVisible().catch(() => false);
+
+ if (isIconVisible) {
+ // If icon is visible, our deleted resource should not be in the drawer
+ await learningIcon.click();
+ await expect(page.locator('.learning-drawer')).toBeVisible();
+ await expect(page.getByText(`Delete E2E Resource ${uniqueId}`)).not.toBeVisible();
+ await page.keyboard.press('Escape');
+ }
+ // If icon is not visible at all, that's also valid (no resources for glossary)
+ });
+
+ await afterAction();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/learning/LearningResourceClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/learning/LearningResourceClass.ts
new file mode 100644
index 000000000000..18e8320012aa
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/playwright/support/learning/LearningResourceClass.ts
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { APIRequestContext } from '@playwright/test';
+import { uuid } from '../../utils/common';
+
+type LearningResourceContext = {
+ pageId: string;
+ componentId?: string;
+};
+
+type LearningResourceSource = {
+ url: string;
+ provider?: string;
+ embedConfig?: Record;
+};
+
+type LearningResourceData = {
+ name: string;
+ displayName?: string;
+ description?: string;
+ resourceType: 'Article' | 'Video' | 'Storylane';
+ categories: string[];
+ difficulty?: 'Intro' | 'Intermediate' | 'Advanced';
+ source: LearningResourceSource;
+ contexts: LearningResourceContext[];
+ status?: 'Draft' | 'Active' | 'Deprecated';
+ estimatedDuration?: number;
+};
+
+type ResponseDataType = LearningResourceData & {
+ id?: string;
+ fullyQualifiedName?: string;
+};
+
+export class LearningResourceClass {
+ id: string;
+ data: LearningResourceData;
+ responseData: ResponseDataType = {} as ResponseDataType;
+
+ constructor(data?: Partial) {
+ this.id = uuid();
+ this.data = {
+ name: `PW_LearningResource_${this.id}`,
+ displayName: `PW Learning Resource ${this.id}`,
+ description: 'Playwright test learning resource description',
+ resourceType: 'Video',
+ categories: ['Discovery'],
+ difficulty: 'Intro',
+ source: {
+ url: 'https://www.youtube.com/watch?v=test123',
+ provider: 'YouTube',
+ },
+ contexts: [{ pageId: 'glossary' }],
+ status: 'Active',
+ ...data,
+ };
+ }
+
+ get() {
+ return this.responseData;
+ }
+
+ async create(apiContext: APIRequestContext) {
+ const response = await apiContext.post('/api/v1/learning/resources', {
+ data: this.data,
+ });
+
+ if (!response.ok()) {
+ const errorText = await response.text();
+ throw new Error(
+ `Failed to create learning resource: ${response.status()} ${errorText}`
+ );
+ }
+
+ const data = await response.json();
+ this.responseData = data;
+
+ return data;
+ }
+
+ async patch(
+ apiContext: APIRequestContext,
+ patchData: Partial
+ ) {
+ const response = await apiContext.patch(
+ `/api/v1/learning/resources/${this.responseData.id}`,
+ {
+ data: [
+ ...Object.entries(patchData).map(([key, value]) => ({
+ op: 'replace',
+ path: `/${key}`,
+ value,
+ })),
+ ],
+ headers: {
+ 'Content-Type': 'application/json-patch+json',
+ },
+ }
+ );
+ const data = await response.json();
+ this.responseData = data;
+
+ return data;
+ }
+
+ async delete(apiContext: APIRequestContext) {
+ const response = await apiContext.delete(
+ `/api/v1/learning/resources/${this.responseData.id}?hardDelete=true`
+ );
+
+ return await response.json();
+ }
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/artical.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/artical.svg
new file mode 100644
index 000000000000..37a1f0f65272
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/artical.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-learning.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-learning.svg
new file mode 100644
index 000000000000..6acef7c8432d
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-learning.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/learning-colored.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/learning-colored.svg
new file mode 100644
index 000000000000..f55c0833e900
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/learning-colored.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/story-lane.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/story-lane.svg
new file mode 100644
index 000000000000..17925afacbfd
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/story-lane.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/video.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/video.svg
new file mode 100644
index 000000000000..ae04987fa8e7
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/video.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/SettingsRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/SettingsRouter.tsx
index 4fdfc487e73e..a8e730e135b8 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/SettingsRouter.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/SettingsRouter.tsx
@@ -39,6 +39,7 @@ import EditEmailConfigPage from '../../pages/EditEmailConfigPage/EditEmailConfig
import EmailConfigSettingsPage from '../../pages/EmailConfigSettingsPage/EmailConfigSettingsPage.component';
import GlobalSettingCategoryPage from '../../pages/GlobalSettingPage/GlobalSettingCategory/GlobalSettingCategoryPage';
import GlobalSettingPage from '../../pages/GlobalSettingPage/GlobalSettingPage';
+import { LearningResourcesPage } from '../../pages/LearningResourcesPage/LearningResourcesPage';
import LineageConfigPage from '../../pages/LineageConfigPage/LineageConfigPage';
import NotificationListPage from '../../pages/NotificationListPage/NotificationListPage';
import OmHealthPage from '../../pages/OmHealth/OmHealthPage';
@@ -646,6 +647,17 @@ const SettingsRouter = () => {
GlobalSettingOptions.OM_HEALTH
)}
/>
+
+
+
+ }
+ path={getSettingPathRelative(
+ GlobalSettingsMenuCategory.PREFERENCES,
+ GlobalSettingOptions.LEARNING_RESOURCES
+ )}
+ />
}
path={getSettingPathRelative(GlobalSettingsMenuCategory.SSO)}
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 1491c3918f4b..d9cfdcd18d68 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
@@ -97,6 +97,7 @@ import RetentionPeriod from '../../Database/RetentionPeriod/RetentionPeriod.comp
import { EntityStatusBadge } from '../../Entity/EntityStatusBadge/EntityStatusBadge.component';
import Voting from '../../Entity/Voting/Voting.component';
import { VotingDataProps } from '../../Entity/Voting/voting.interface';
+import { LearningIcon } from '../../Learning/LearningIcon/LearningIcon.component';
import MetricHeaderInfo from '../../Metric/MetricHeaderInfo/MetricHeaderInfo';
import SuggestionsAlert from '../../Suggestions/SuggestionsAlert/SuggestionsAlert';
import { useSuggestionsContext } from '../../Suggestions/SuggestionsProvider/SuggestionsProvider';
@@ -622,6 +623,7 @@ export const DataAssetsHeader = ({
isFollowingLoading={isFollowingLoading}
name={dataAsset?.name}
serviceName={dataAssetServiceName}
+ suffix={ }
/>
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx
index 8cc88055d65e..76ddebf3e083 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx
@@ -132,6 +132,7 @@ const DataProductListPage = () => {
createPermission: permissions.dataProduct?.Create || false,
addButtonLabelKey: 'label.add-data-product',
onAddClick: openDrawer,
+ learningPageId: 'dataProduct',
});
const { titleAndCount } = useTitleAndCount({
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx
index 946113577cd9..2665f23b2515 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx
@@ -51,9 +51,7 @@ import { useCustomPages } from '../../../hooks/useCustomPages';
import { useFqn } from '../../../hooks/useFqn';
import { FeedCounts } from '../../../interface/feed.interface';
import { QueryFilterInterface } from '../../../pages/ExplorePage/ExplorePage.interface';
-import {
- getDataProductPortsView,
-} from '../../../rest/dataProductAPI';
+import { getDataProductPortsView } from '../../../rest/dataProductAPI';
import { getActiveAnnouncement } from '../../../rest/feedsAPI';
import { searchQuery } from '../../../rest/searchAPI';
import {
@@ -100,6 +98,7 @@ import { EntityStatusBadge } from '../../Entity/EntityStatusBadge/EntityStatusBa
import { EntityDetailsObjectInterface } from '../../Explore/ExplorePage.interface';
import { AssetsTabRef } from '../../Glossary/GlossaryTerms/tabs/AssetsTabs.component';
import { AssetsOfEntity } from '../../Glossary/GlossaryTerms/tabs/AssetsTabs.interface';
+import { LearningIcon } from '../../Learning/LearningIcon/LearningIcon.component';
import EntityDeleteModal from '../../Modals/EntityDeleteModal/EntityDeleteModal';
import EntityNameModal from '../../Modals/EntityNameModal/EntityNameModal.component';
import StyleModal from '../../Modals/StyleModal/StyleModal.component';
@@ -676,6 +675,7 @@ const DataProductsDetailsPage = ({
isFollowing={isFollowing}
isFollowingLoading={isFollowingLoading}
serviceName=""
+ suffix={ }
titleColor={dataProduct.style?.color}
/>
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/InputOutputPortsTab/InputOutputPortsTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/InputOutputPortsTab/InputOutputPortsTab.component.tsx
index bdf241ecb1d4..8409d25c6a85 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/InputOutputPortsTab/InputOutputPortsTab.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/InputOutputPortsTab/InputOutputPortsTab.component.tsx
@@ -22,6 +22,7 @@ import {
useTheme,
} from '@mui/material';
import { ChevronDown, ChevronUp } from '@untitledui/icons';
+import { Button } from 'antd';
import { AxiosError } from 'axios';
import React, {
forwardRef,
@@ -48,7 +49,6 @@ import {
} from './InputOutputPortsTab.types';
import { PortsLineageView } from './PortsLineageView';
import { PortsListView, PortsListViewRef } from './PortsListView';
-import { Button } from 'antd';
export const InputOutputPortsTab = forwardRef<
InputOutputPortsTabRef,
@@ -334,7 +334,7 @@ export const InputOutputPortsTab = forwardRef<
{permissions.EditAll && !isInputPortsCollapsed && (
{
e.stopPropagation();
@@ -433,7 +433,7 @@ export const InputOutputPortsTab = forwardRef<
{permissions.EditAll && !isOutputPortsCollapsed && (
{
e.stopPropagation();
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/InputOutputPortsTab/PortsListView/PortsListView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/InputOutputPortsTab/PortsListView/PortsListView.component.tsx
index 8563690a0a88..7e185f452363 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/InputOutputPortsTab/PortsListView/PortsListView.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/InputOutputPortsTab/PortsListView/PortsListView.component.tsx
@@ -210,12 +210,9 @@ const PortsListView = forwardRef(
key: 'delete',
label: (
}
titleColor={domain.style?.color}
/>
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx
index 45a6f5a4159f..963c4a8a9d98 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx
@@ -135,6 +135,7 @@ const DomainListPage = () => {
addButtonLabelKey: 'label.add-domain',
addButtonTestId: 'add-domain',
onAddClick: openDrawer,
+ learningPageId: 'domain',
});
const { titleAndCount } = useTitleAndCount({
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityHeader/EntityHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityHeader/EntityHeader.component.tsx
index 2246ee270d98..9060e4442bc5 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityHeader/EntityHeader.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityHeader/EntityHeader.component.tsx
@@ -35,6 +35,7 @@ interface Props {
serviceName: string;
titleColor?: string;
badge?: React.ReactNode;
+ suffix?: React.ReactNode;
showName?: boolean;
nameClassName?: string;
displayNameClassName?: string;
@@ -55,6 +56,7 @@ export const EntityHeader = ({
gutter = 'default',
serviceName,
badge,
+ suffix,
titleColor,
showName = true,
isFollowingLoading,
@@ -98,6 +100,7 @@ export const EntityHeader = ({
serviceName={serviceName}
showName={showName}
showOnlyDisplayName={showOnlyDisplayName}
+ suffix={suffix}
/>
);
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityHeaderTitle/EntityHeaderTitle.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityHeaderTitle/EntityHeaderTitle.component.tsx
index 5d2494fb7557..6ece137a0b4a 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityHeaderTitle/EntityHeaderTitle.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityHeaderTitle/EntityHeaderTitle.component.tsx
@@ -39,6 +39,7 @@ const EntityHeaderTitle = ({
deleted = false,
serviceName,
badge,
+ suffix,
isDisabled,
className,
showName = true,
@@ -146,6 +147,7 @@ const EntityHeaderTitle = ({
{badges}
+ {suffix}
) : null}
@@ -183,6 +185,7 @@ const EntityHeaderTitle = ({
onClick={handleShareButtonClick}
/>
+ {(isEmpty(displayName) || !showName) && suffix}
{!excludeEntityService &&
!deleted &&
!isCustomizedView &&
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityHeaderTitle/EntityHeaderTitle.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityHeaderTitle/EntityHeaderTitle.interface.ts
index 4a24b9a999c9..90e088539c7a 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityHeaderTitle/EntityHeaderTitle.interface.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityHeaderTitle/EntityHeaderTitle.interface.ts
@@ -25,6 +25,7 @@ export interface EntityHeaderTitleProps {
deleted?: boolean;
serviceName: string;
badge?: React.ReactNode;
+ suffix?: React.ReactNode;
isDisabled?: boolean;
showName?: boolean;
excludeEntityService?: boolean;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx
index 9ccebfd9a801..3df2b14a3485 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx
@@ -73,6 +73,7 @@ import { TitleBreadcrumbProps } from '../../common/TitleBreadcrumb/TitleBreadcru
import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider';
import { EntityStatusBadge } from '../../Entity/EntityStatusBadge/EntityStatusBadge.component';
import Voting from '../../Entity/Voting/Voting.component';
+import { LearningIcon } from '../../Learning/LearningIcon/LearningIcon.component';
import ChangeParentHierarchy from '../../Modals/ChangeParentHierarchy/ChangeParentHierarchy.component';
import StyleModal from '../../Modals/StyleModal/StyleModal.component';
import { GlossaryHeaderProps } from './GlossaryHeader.interface';
@@ -535,6 +536,9 @@ const GlossaryHeader = ({
entityType={EntityType.GLOSSARY_TERM}
icon={icon}
serviceName=""
+ suffix={
+
+ }
titleColor={isGlossary ? undefined : selectedData.style?.color}
/>
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface.ts
index 502238f6bf3f..73953db4a5ba 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface.ts
@@ -12,8 +12,8 @@
*/
import { OperationPermission } from '../../../../context/PermissionProvider/PermissionProvider.interface';
-import { SearchedDataProps } from '../../../SearchedData/SearchedData.interface';
import { EntityDetailsObjectInterface } from '../../../Explore/ExplorePage.interface';
+import { SearchedDataProps } from '../../../SearchedData/SearchedData.interface';
export enum AssetsOfEntity {
GLOSSARY = 'GLOSSARY',
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/Learning.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Learning/Learning.interface.ts
new file mode 100644
index 000000000000..c844b0601347
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/Learning.interface.ts
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type ResourceCategory =
+ | 'Discovery'
+ | 'Administration'
+ | 'DataGovernance'
+ | 'DataQuality'
+ | 'Observability'
+ | 'AI';
+
+export type ResourceType = 'Storylane' | 'Video' | 'Article';
+
+export type ResourceDifficulty = 'Intro' | 'Intermediate' | 'Advanced';
+
+export interface CategoryInfo {
+ key: ResourceCategory;
+ label: string;
+ description: string;
+ icon: string;
+ color: string;
+ bgColor: string;
+ borderColor: string;
+}
+
+export const LEARNING_CATEGORIES: Record = {
+ Discovery: {
+ key: 'Discovery',
+ label: 'Discovery',
+ description: 'Learn how to discover and explore data assets',
+ icon: 'search',
+ color: '#175cd3',
+ bgColor: '#eff8ff',
+ borderColor: '#b2ddff',
+ },
+ Administration: {
+ key: 'Administration',
+ label: 'Admin',
+ description: 'Manage users, teams, and system configuration',
+ icon: 'setting',
+ color: '#026aa2',
+ bgColor: '#f0f9ff',
+ borderColor: '#b9e6fe',
+ },
+ DataGovernance: {
+ key: 'DataGovernance',
+ label: 'Governance',
+ description: 'Implement governance policies and workflows',
+ icon: 'shield',
+ color: '#5925dc',
+ bgColor: '#f4f3ff',
+ borderColor: '#d9d6fe',
+ },
+ DataQuality: {
+ key: 'DataQuality',
+ label: 'Data Quality',
+ description: 'Monitor data quality and set up tests',
+ icon: 'dashboard',
+ color: '#b93815',
+ bgColor: '#fef6ee',
+ borderColor: '#f9dbaf',
+ },
+ Observability: {
+ key: 'Observability',
+ label: 'Observability',
+ description: 'Monitor system health and performance',
+ icon: 'eye',
+ color: '#b93815',
+ bgColor: '#fef6ee',
+ borderColor: '#f9dbaf',
+ },
+ AI: {
+ key: 'AI',
+ label: 'AI',
+ description: 'AI-powered features and assistants',
+ icon: 'robot',
+ color: '#7c3aed',
+ bgColor: '#f5f3ff',
+ borderColor: '#ddd6fe',
+ },
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningDrawer/LearningDrawer.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningDrawer/LearningDrawer.component.tsx
new file mode 100644
index 000000000000..a9d46ae8e3bc
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningDrawer/LearningDrawer.component.tsx
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { CloseOutlined, LoadingOutlined } from '@ant-design/icons';
+import { Drawer, Empty, Spin, Typography } from 'antd';
+import React, { useCallback, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import {
+ getLearningResourcesByContext,
+ LearningResource,
+} from '../../../rest/learningResourceAPI';
+import { LearningResourceCard } from '../LearningResourceCard/LearningResourceCard.component';
+import { ResourcePlayerModal } from '../ResourcePlayer/ResourcePlayerModal.component';
+import './learning-drawer.less';
+import { LearningDrawerProps } from './LearningDrawer.interface';
+
+const { Title } = Typography;
+
+export const LearningDrawer: React.FC = ({
+ open,
+ pageId,
+ title,
+ onClose,
+}) => {
+ const { t } = useTranslation();
+ const [resources, setResources] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [hasError, setHasError] = useState(false);
+ const [selectedResource, setSelectedResource] =
+ useState(null);
+ const [playerOpen, setPlayerOpen] = useState(false);
+
+ const fetchResources = useCallback(async () => {
+ if (!open || !pageId) {
+ return;
+ }
+
+ setIsLoading(true);
+ setHasError(false);
+ try {
+ const response = await getLearningResourcesByContext(pageId, {
+ limit: 50,
+ fields: 'categories,contexts,difficulty,estimatedDuration',
+ });
+ setResources(response.data || []);
+ } catch {
+ setHasError(true);
+ setResources([]);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [open, pageId]);
+
+ useEffect(() => {
+ if (open) {
+ fetchResources();
+ }
+ }, [open, fetchResources]);
+
+ const handleResourceClick = useCallback((resource: LearningResource) => {
+ setSelectedResource(resource);
+ setPlayerOpen(true);
+ }, []);
+
+ const handlePlayerClose = useCallback(() => {
+ setPlayerOpen(false);
+ setSelectedResource(null);
+ }, []);
+
+ const getPageTitle = useCallback(() => {
+ if (title) {
+ return title;
+ }
+
+ const titleMap: Record = {
+ glossary: t('label.glossary'),
+ glossaryTerm: t('label.glossary-term'),
+ domain: t('label.domain'),
+ dataProduct: t('label.data-product'),
+ dataQuality: t('label.data-quality'),
+ observability: t('label.observability'),
+ governance: t('label.governance'),
+ discovery: t('label.discovery'),
+ administration: t('label.administration'),
+ };
+
+ return titleMap[pageId] || pageId;
+ }, [pageId, title, t]);
+
+ return (
+ <>
+
+
+ {t('label.entity-resource', { entity: getPageTitle() })}
+
+
+
+ }
+ width={576}
+ onClose={onClose}>
+
+ {isLoading ? (
+
+ }
+ />
+
+ ) : hasError ? (
+
+ ) : resources.length === 0 ? (
+
+ ) : (
+
+ {resources.map((resource) => (
+
+ ))}
+
+ )}
+
+
+
+ {selectedResource && (
+
+ )}
+ >
+ );
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningDrawer/LearningDrawer.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningDrawer/LearningDrawer.interface.ts
new file mode 100644
index 000000000000..69779c41e889
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningDrawer/LearningDrawer.interface.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface LearningDrawerProps {
+ open: boolean;
+ pageId: string;
+ title?: string;
+ onClose: () => void;
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningDrawer/LearningDrawer.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningDrawer/LearningDrawer.test.tsx
new file mode 100644
index 000000000000..4b33b8c93d6d
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningDrawer/LearningDrawer.test.tsx
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { render, screen, waitFor } from '@testing-library/react';
+import { getLearningResourcesByContext } from '../../../rest/learningResourceAPI';
+import { LearningDrawer } from './LearningDrawer.component';
+
+jest.mock('../../../rest/learningResourceAPI', () => ({
+ getLearningResourcesByContext: jest.fn(),
+}));
+
+jest.mock('../LearningResourceCard/LearningResourceCard.component', () => ({
+ LearningResourceCard: jest
+ .fn()
+ .mockImplementation(({ resource }) => (
+ {resource.name}
+ )),
+}));
+
+jest.mock('../ResourcePlayer/ResourcePlayerModal.component', () => ({
+ ResourcePlayerModal: jest
+ .fn()
+ .mockImplementation(() =>
),
+}));
+
+const mockOnClose = jest.fn();
+
+const mockResources = [
+ {
+ id: '1',
+ name: 'Test Resource 1',
+ displayName: 'Test Resource 1',
+ resourceType: 'Video',
+ categories: ['Discovery'],
+ source: { url: 'https://example.com/video1' },
+ contexts: [{ pageId: 'glossary' }],
+ },
+ {
+ id: '2',
+ name: 'Test Resource 2',
+ displayName: 'Test Resource 2',
+ resourceType: 'Article',
+ categories: ['DataGovernance'],
+ source: { url: 'https://example.com/article' },
+ contexts: [{ pageId: 'glossary' }],
+ },
+];
+
+describe('LearningDrawer', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render loading state initially', () => {
+ (getLearningResourcesByContext as jest.Mock).mockImplementation(
+ () => new Promise(() => {})
+ );
+
+ render(
+
+ );
+
+ expect(screen.getByTestId('loader')).toBeInTheDocument();
+ });
+
+ it('should render resources when loaded successfully', async () => {
+ (getLearningResourcesByContext as jest.Mock).mockResolvedValue({
+ data: mockResources,
+ });
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getAllByTestId('learning-resource-card')).toHaveLength(2);
+ });
+ });
+
+ it('should render empty state when no resources found', async () => {
+ (getLearningResourcesByContext as jest.Mock).mockResolvedValue({
+ data: [],
+ });
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('message.no-learning-resources-available')
+ ).toBeInTheDocument();
+ });
+ });
+
+ it('should render error state when API fails', async () => {
+ (getLearningResourcesByContext as jest.Mock).mockRejectedValue(
+ new Error('API Error')
+ );
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('message.failed-to-load-learning-resources')
+ ).toBeInTheDocument();
+ });
+ });
+
+ it('should not fetch resources when drawer is closed', () => {
+ render(
+
+ );
+
+ expect(getLearningResourcesByContext).not.toHaveBeenCalled();
+ });
+
+ it('should render close button', async () => {
+ (getLearningResourcesByContext as jest.Mock).mockResolvedValue({
+ data: mockResources,
+ });
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('close-drawer')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningDrawer/learning-drawer.less b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningDrawer/learning-drawer.less
new file mode 100644
index 000000000000..688a4713e1f4
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningDrawer/learning-drawer.less
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import (reference) '../../../styles/variables.less';
+
+.learning-drawer {
+ .ant-drawer-header {
+ padding: @padding-md @padding-lg;
+ border-bottom: 1px solid @border-color-1;
+ }
+
+ .ant-drawer-header-title {
+ flex: 1;
+ }
+
+ .ant-drawer-body {
+ padding: @padding-md;
+ background-color: @grey-50;
+ }
+
+ .learning-drawer-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ }
+
+ .learning-drawer-title {
+ margin: 0;
+ font-size: 16px;
+ font-weight: @font-semibold;
+ color: @grey-900;
+ }
+
+ .learning-drawer-close {
+ font-size: 14px;
+ color: @grey-500;
+ cursor: pointer;
+ padding: @padding-xs;
+ border-radius: @border-rad-base;
+ transition: all 0.2s ease;
+
+ &:hover {
+ color: @grey-700;
+ background-color: @grey-100;
+ }
+ }
+
+ .learning-drawer-content {
+ height: 100%;
+ }
+
+ .learning-drawer-cards {
+ display: flex;
+ flex-direction: column;
+ gap: @size-sm;
+ }
+
+ .learning-drawer-loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 200px;
+ }
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningIcon/LearningIcon.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningIcon/LearningIcon.component.tsx
new file mode 100644
index 000000000000..4d8253560e43
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningIcon/LearningIcon.component.tsx
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ArrowRightOutlined } from '@ant-design/icons';
+import { Badge, Button, Popover } from 'antd';
+import classNames from 'classnames';
+import React, { useCallback, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { ReactComponent as LearningIconSvg } from '../../../assets/svg/ic-learning.svg';
+import { getLearningResourcesByContext } from '../../../rest/learningResourceAPI';
+import { LearningDrawer } from '../LearningDrawer/LearningDrawer.component';
+import './learning-icon.less';
+import { LearningIconProps } from './LearningIcon.interface';
+
+export const LearningIcon: React.FC = ({
+ pageId,
+ title,
+ className = '',
+}) => {
+ const { t } = useTranslation();
+ const [drawerOpen, setDrawerOpen] = useState(false);
+ const [resourceCount, setResourceCount] = useState(0);
+ const [isLoading, setIsLoading] = useState(true);
+ const [hasError, setHasError] = useState(false);
+
+ const fetchResourceCount = useCallback(async () => {
+ if (resourceCount > 0 || hasError) {
+ return;
+ }
+ setIsLoading(true);
+ try {
+ const response = await getLearningResourcesByContext(pageId, {
+ limit: 1,
+ });
+ setResourceCount(response.paging?.total ?? 0);
+ } catch {
+ setHasError(true);
+ setResourceCount(0);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [pageId, resourceCount, hasError]);
+
+ useEffect(() => {
+ fetchResourceCount();
+ }, []);
+
+ const handleClick = useCallback(() => {
+ setDrawerOpen(true);
+ }, []);
+
+ const handleClose = useCallback(() => {
+ setDrawerOpen(false);
+ }, []);
+
+ if (hasError || (resourceCount === 0 && !isLoading)) {
+ return null;
+ }
+
+ const popoverContent = (
+
+
+ {t('label.learn-how-this-feature-works')}
+
+
+ {resourceCount} {t('label.resource-plural').toLowerCase()}{' '}
+
+
+
+ );
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningIcon/LearningIcon.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningIcon/LearningIcon.interface.ts
new file mode 100644
index 000000000000..13bfd18000b2
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningIcon/LearningIcon.interface.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface LearningIconProps {
+ pageId: string;
+ title?: string;
+ className?: string;
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningIcon/LearningIcon.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningIcon/LearningIcon.test.tsx
new file mode 100644
index 000000000000..c8249b168c43
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningIcon/LearningIcon.test.tsx
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ act,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from '@testing-library/react';
+import { LearningIcon } from './LearningIcon.component';
+
+const mockGetLearningResourcesByContext = jest.fn();
+
+jest.mock('../../../rest/learningResourceAPI', () => ({
+ getLearningResourcesByContext: jest
+ .fn()
+ .mockImplementation((...args) =>
+ mockGetLearningResourcesByContext(...args)
+ ),
+}));
+
+jest.mock('../LearningDrawer/LearningDrawer.component', () => ({
+ LearningDrawer: jest.fn().mockImplementation(({ open, onClose }) =>
+ open ? (
+
+
+ Close
+
+
+ ) : null
+ ),
+}));
+
+jest.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}));
+
+describe('LearningIcon', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockGetLearningResourcesByContext.mockResolvedValue({
+ data: [],
+ paging: { total: 5 },
+ });
+ });
+
+ it('should render learning icon when resources exist', async () => {
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('learning-icon')).toBeInTheDocument();
+ });
+ });
+
+ it('should not render when resource count is 0', async () => {
+ mockGetLearningResourcesByContext.mockResolvedValue({
+ data: [],
+ paging: { total: 0 },
+ });
+
+ const { container } = render( );
+
+ await waitFor(() => {
+ expect(mockGetLearningResourcesByContext).toHaveBeenCalled();
+ });
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('learning-icon')).not.toBeInTheDocument();
+ });
+
+ expect(container.firstChild).toBeNull();
+ });
+
+ it('should open drawer when icon is clicked', async () => {
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('learning-icon')).toBeInTheDocument();
+ });
+
+ const iconContainer = screen.getByTestId('learning-icon');
+
+ await act(async () => {
+ fireEvent.click(iconContainer);
+ });
+
+ expect(screen.getByTestId('learning-drawer')).toBeInTheDocument();
+ });
+
+ it('should close drawer when close button is clicked', async () => {
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('learning-icon')).toBeInTheDocument();
+ });
+
+ const iconContainer = screen.getByTestId('learning-icon');
+
+ await act(async () => {
+ fireEvent.click(iconContainer);
+ });
+
+ expect(screen.getByTestId('learning-drawer')).toBeInTheDocument();
+
+ const closeButton = screen.getByTestId('close-drawer');
+
+ await act(async () => {
+ fireEvent.click(closeButton);
+ });
+
+ expect(screen.queryByTestId('learning-drawer')).not.toBeInTheDocument();
+ });
+
+ it('should fetch resource count on mount', async () => {
+ render( );
+
+ await waitFor(() => {
+ expect(mockGetLearningResourcesByContext).toHaveBeenCalledWith(
+ 'glossary',
+ { limit: 1 }
+ );
+ });
+ });
+
+ it('should not fetch resource count again if already fetched', async () => {
+ render( );
+
+ await waitFor(() => {
+ expect(mockGetLearningResourcesByContext).toHaveBeenCalledTimes(1);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('learning-icon')).toBeInTheDocument();
+ });
+
+ expect(mockGetLearningResourcesByContext).toHaveBeenCalledTimes(1);
+ });
+
+ it('should handle API error gracefully', async () => {
+ mockGetLearningResourcesByContext.mockRejectedValueOnce(
+ new Error('API Error')
+ );
+
+ const { container } = render( );
+
+ await waitFor(() => {
+ expect(mockGetLearningResourcesByContext).toHaveBeenCalled();
+ });
+
+ await waitFor(() => {
+ expect(container.firstChild).toBeNull();
+ });
+ });
+
+ it('should apply custom className', async () => {
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('learning-icon')).toBeInTheDocument();
+ });
+
+ const badge = screen
+ .getByTestId('learning-icon')
+ .closest('.learning-icon-badge');
+
+ expect(badge).toHaveClass('custom-class');
+ });
+
+ it('should display resource count in badge', async () => {
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('learning-icon')).toBeInTheDocument();
+ });
+
+ expect(screen.getByText('5')).toBeInTheDocument();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningIcon/learning-icon.less b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningIcon/learning-icon.less
new file mode 100644
index 000000000000..a4f2f2e7cdfa
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningIcon/learning-icon.less
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import (reference) '../../../styles/variables.less';
+
+.learning-icon-container {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ transform: scale(1.1);
+ }
+}
+
+.ant-badge.learning-icon-badge {
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ vertical-align: middle;
+ position: relative;
+ border-radius: 16px;
+ padding: 4px !important;
+
+ .ant-badge-count {
+ background-color: @red-14;
+ font-size: 10px;
+ min-width: 16px;
+ height: 16px;
+ line-height: 16px;
+ padding: 0 4px;
+ box-shadow: 0 0 0 1px @white;
+ position: absolute !important;
+ top: -4px !important;
+ right: -6px !important;
+ transform: none !important;
+ }
+}
+
+.learning-tooltip-popover {
+ .ant-popover-inner {
+ padding: 0;
+ border-radius: @border-rad-sm;
+ background-color: @grey-50;
+ }
+}
+
+.learning-tooltip-content {
+ display: flex;
+ align-items: center;
+ gap: @size-sm;
+
+ .learning-tooltip-text {
+ font-size: 13px;
+ white-space: nowrap;
+ }
+
+ .learning-tooltip-button {
+ font-size: 12px;
+ border-radius: @border-rad-sm;
+ background-color: @white;
+ border: none;
+ color: @grey-700;
+ }
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningResourceCard/LearningResourceCard.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningResourceCard/LearningResourceCard.component.tsx
new file mode 100644
index 000000000000..b891d806f1f9
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningResourceCard/LearningResourceCard.component.tsx
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Tag, Typography } from 'antd';
+import classNames from 'classnames';
+import { DateTime } from 'luxon';
+import React, { useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { ReactComponent as ArticalIcon } from '../../../assets/svg/artical.svg';
+import { ReactComponent as StoryLaneIcon } from '../../../assets/svg/story-lane.svg';
+import { ReactComponent as VideoIcon } from '../../../assets/svg/video.svg';
+import { LEARNING_CATEGORIES } from '../Learning.interface';
+import './learning-resource-card.less';
+import { LearningResourceCardProps } from './LearningResourceCard.interface';
+
+const { Text, Paragraph, Link } = Typography;
+
+const MAX_VISIBLE_TAGS = 3;
+
+export const LearningResourceCard: React.FC = ({
+ resource,
+ onClick,
+}) => {
+ const { t } = useTranslation();
+ const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
+ const [isDescriptionTruncated, setIsDescriptionTruncated] = useState(false);
+
+ const resourceTypeIcon = useMemo(() => {
+ switch (resource.resourceType) {
+ case 'Video':
+ return ;
+ case 'Storylane':
+ return ;
+ case 'Article':
+ return ;
+ default:
+ return ;
+ }
+ }, [resource.resourceType]);
+
+ const formattedDuration = useMemo(() => {
+ if (!resource.estimatedDuration) {
+ return null;
+ }
+ const minutes = Math.floor(resource.estimatedDuration / 60);
+
+ return `${minutes} ${t('label.min-read')}`;
+ }, [resource.estimatedDuration, t]);
+
+ const formattedDate = useMemo(() => {
+ if (!resource.updatedAt) {
+ return null;
+ }
+
+ return DateTime.fromMillis(resource.updatedAt).toFormat('LLL dd, yyyy');
+ }, [resource.updatedAt]);
+
+ const categoryTags = useMemo(() => {
+ if (!resource.categories || resource.categories.length === 0) {
+ return { visible: [], remaining: 0 };
+ }
+
+ const visible = resource.categories.slice(0, MAX_VISIBLE_TAGS);
+ const remaining = resource.categories.length - MAX_VISIBLE_TAGS;
+
+ return { visible, remaining };
+ }, [resource.categories]);
+
+ const getCategoryColor = (category: string) => {
+ const categoryInfo =
+ LEARNING_CATEGORIES[category as keyof typeof LEARNING_CATEGORIES];
+
+ return categoryInfo?.color ?? '#1890ff';
+ };
+
+ const handleViewMoreClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setIsDescriptionExpanded(!isDescriptionExpanded);
+ };
+
+ return (
+ onClick?.(resource)}>
+
+
+ {resourceTypeIcon}
+
+ {resource.displayName || resource.name}
+
+
+
+ {resource.description && (
+
+
+ {resource.description}
+
+ {(isDescriptionTruncated || isDescriptionExpanded) && (
+
+ {isDescriptionExpanded
+ ? t('label.view-less')
+ : t('label.view-more')}
+
+ )}
+
+ )}
+
+
+
+ {categoryTags.visible.map((category) => (
+
+ {LEARNING_CATEGORIES[
+ category as keyof typeof LEARNING_CATEGORIES
+ ]?.label ?? category}
+
+ ))}
+ {categoryTags.remaining > 0 && (
+
+ +{categoryTags.remaining}
+
+ )}
+
+
+
+ {formattedDate && (
+
+ {formattedDate}
+
+ )}
+ {formattedDate && formattedDuration && (
+ |
+ )}
+ {formattedDuration && (
+
+ {formattedDuration}
+
+ )}
+
+
+
+
+ );
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningResourceCard/LearningResourceCard.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningResourceCard/LearningResourceCard.interface.ts
new file mode 100644
index 000000000000..5d8085802378
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningResourceCard/LearningResourceCard.interface.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { LearningResource } from '../../../rest/learningResourceAPI';
+
+export interface LearningResourceCardProps {
+ resource: LearningResource;
+ onClick?: (resource: LearningResource) => void;
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningResourceCard/LearningResourceCard.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningResourceCard/LearningResourceCard.test.tsx
new file mode 100644
index 000000000000..4915d45560cc
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningResourceCard/LearningResourceCard.test.tsx
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { fireEvent, render, screen } from '@testing-library/react';
+import { LearningResource } from '../../../rest/learningResourceAPI';
+import { LearningResourceCard } from './LearningResourceCard.component';
+
+jest.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string) => {
+ const translations: Record = {
+ 'label.min-read': 'min read',
+ 'label.view-more': 'View More',
+ 'label.view-less': 'View Less',
+ };
+
+ return translations[key] ?? key;
+ },
+ }),
+}));
+
+const mockVideoResource: LearningResource = {
+ id: 'video-resource-1',
+ name: 'TestVideoResource',
+ displayName: 'Test Video Resource',
+ description: 'A test video learning resource',
+ resourceType: 'Video',
+ categories: ['Discovery'],
+ difficulty: 'Intro',
+ estimatedDuration: 300,
+ source: {
+ url: 'https://youtube.com/watch?v=test',
+ provider: 'YouTube',
+ },
+ contexts: [{ pageId: 'glossary' }],
+ status: 'Active',
+ fullyQualifiedName: 'TestVideoResource',
+ version: 0.1,
+ updatedAt: Date.now(),
+ updatedBy: 'admin',
+};
+
+const mockStorylaneResource: LearningResource = {
+ ...mockVideoResource,
+ id: 'storylane-resource-1',
+ name: 'TestStorylaneResource',
+ displayName: 'Test Storylane Resource',
+ resourceType: 'Storylane',
+};
+
+const mockArticleResource: LearningResource = {
+ ...mockVideoResource,
+ id: 'article-resource-1',
+ name: 'TestArticleResource',
+ displayName: 'Test Article Resource',
+ resourceType: 'Article',
+};
+
+const mockResourceWithMultipleCategories: LearningResource = {
+ ...mockVideoResource,
+ categories: ['Discovery', 'Administration', 'DataGovernance', 'DataQuality'],
+};
+
+describe('LearningResourceCard', () => {
+ it('should render card with resource display name', () => {
+ render( );
+
+ expect(screen.getByText('Test Video Resource')).toBeInTheDocument();
+ });
+
+ it('should render card with resource name when displayName is not provided', () => {
+ const resourceWithoutDisplayName = {
+ ...mockVideoResource,
+ displayName: undefined,
+ };
+ render( );
+
+ expect(screen.getByText('TestVideoResource')).toBeInTheDocument();
+ });
+
+ it('should render resource description', () => {
+ render( );
+
+ expect(
+ screen.getByText(/A test video learning resource/i)
+ ).toBeInTheDocument();
+ });
+
+ it('should render description with ellipsis configuration', () => {
+ render( );
+
+ const descriptionElement = screen.getByLabelText(
+ /A test video learning resource/i
+ );
+
+ expect(descriptionElement).toBeInTheDocument();
+ expect(descriptionElement).toHaveClass('learning-resource-description');
+ });
+
+ it('should render category tag', () => {
+ render( );
+
+ expect(screen.getByText('Discovery')).toBeInTheDocument();
+ });
+
+ it('should render formatted duration', () => {
+ render( );
+
+ expect(screen.getByText('5 min read')).toBeInTheDocument();
+ });
+
+ it('should not render duration when estimatedDuration is not provided', () => {
+ const resourceWithoutDuration = {
+ ...mockVideoResource,
+ estimatedDuration: undefined,
+ };
+ render( );
+
+ expect(screen.queryByText(/min read/)).not.toBeInTheDocument();
+ });
+
+ it('should render play icon for Video resource type', () => {
+ render( );
+
+ expect(
+ screen.getByTestId(`learning-resource-card-${mockVideoResource.name}`)
+ ).toBeInTheDocument();
+ });
+
+ it('should render rocket icon for Storylane resource type', () => {
+ render( );
+
+ expect(
+ screen.getByTestId(`learning-resource-card-${mockStorylaneResource.name}`)
+ ).toBeInTheDocument();
+ });
+
+ it('should render file icon for Article resource type', () => {
+ render( );
+
+ expect(
+ screen.getByTestId(`learning-resource-card-${mockArticleResource.name}`)
+ ).toBeInTheDocument();
+ });
+
+ it('should call onClick when card is clicked', () => {
+ const mockOnClick = jest.fn();
+ render(
+
+ );
+
+ const card = screen.getByTestId(
+ `learning-resource-card-${mockVideoResource.name}`
+ );
+ fireEvent.click(card);
+
+ expect(mockOnClick).toHaveBeenCalledWith(mockVideoResource);
+ });
+
+ it('should not throw error when onClick is not provided', () => {
+ render( );
+
+ const card = screen.getByTestId(
+ `learning-resource-card-${mockVideoResource.name}`
+ );
+
+ expect(() => fireEvent.click(card)).not.toThrow();
+ });
+
+ it('should have correct data-testid attribute', () => {
+ render( );
+
+ expect(
+ screen.getByTestId(`learning-resource-card-${mockVideoResource.name}`)
+ ).toBeInTheDocument();
+ });
+
+ it('should be clickable when onClick is provided', () => {
+ const mockOnClick = jest.fn();
+ render(
+
+ );
+
+ const card = screen.getByTestId(
+ `learning-resource-card-${mockVideoResource.name}`
+ );
+
+ expect(card).toHaveClass('learning-resource-card-clickable');
+ });
+
+ it('should show only first 3 categories and +N for remaining', () => {
+ render(
+
+ );
+
+ // Categories are mapped to labels: Administration -> Admin, DataGovernance -> Governance
+ expect(screen.getByText('Discovery')).toBeInTheDocument();
+ expect(screen.getByText('Admin')).toBeInTheDocument();
+ expect(screen.getByText('Governance')).toBeInTheDocument();
+ expect(screen.getByText('+1')).toBeInTheDocument();
+ });
+
+ it('should not render description section when description is not provided', () => {
+ const resourceWithoutDescription = {
+ ...mockVideoResource,
+ description: undefined,
+ };
+ render( );
+
+ expect(
+ screen.queryByLabelText(/learning resource/i)
+ ).not.toBeInTheDocument();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningResourceCard/learning-resource-card.less b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningResourceCard/learning-resource-card.less
new file mode 100644
index 000000000000..d0147d359397
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningResourceCard/learning-resource-card.less
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import (reference) '../../../styles/variables.less';
+
+.learning-resource-card {
+ padding: @padding-md;
+ border-radius: @border-rad-sm;
+ background-color: @white;
+ transition: all 0.2s ease;
+ border: 1px solid @border-color-1;
+
+ &.learning-resource-card-clickable {
+ cursor: pointer;
+
+ &:hover {
+ box-shadow: @box-shadow-card-subtle;
+ border-color: @grey-300;
+ }
+ }
+
+ .learning-resource-content {
+ display: flex;
+ flex-direction: column;
+ gap: @size-xs;
+ }
+
+ .learning-resource-header {
+ display: flex;
+ align-items: center;
+ gap: @size-xs;
+
+ .resource-type-icon {
+ font-size: 16px;
+ flex-shrink: 0;
+ margin-top: 2px;
+
+ &.video-icon {
+ color: #1570ef;
+ }
+
+ &.storylane-icon {
+ color: #7147e8;
+ }
+
+ &.article-icon {
+ color: #17b26a;
+ }
+ }
+
+ .learning-resource-title {
+ font-size: @font-size-base;
+ line-height: 20px;
+ color: @grey-900;
+ }
+ }
+
+ .learning-resource-description-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: @size-xxs;
+ align-items: baseline;
+ }
+
+ .learning-resource-description {
+ margin: 0;
+ font-size: 12px;
+ line-height: 18px;
+ color: @grey-600;
+ display: inline;
+ }
+
+ .view-more-link {
+ font-size: 12px;
+ color: @primary-color;
+ cursor: pointer;
+ flex-shrink: 0;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ .learning-resource-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: @size-xs;
+ margin-top: @size-xxs;
+ }
+
+ .learning-resource-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: @size-xxs;
+
+ .category-tag {
+ margin: 0;
+ font-size: 11px;
+ line-height: 16px;
+ padding: 2px 6px;
+ border-radius: @border-rad-base;
+ font-weight: @font-medium;
+
+ &.more-tag {
+ background-color: @grey-100;
+ border-color: @grey-300;
+ color: @grey-600;
+ }
+ }
+ }
+
+ .learning-resource-meta {
+ display: flex;
+ align-items: center;
+ gap: @size-xxs;
+ flex-shrink: 0;
+
+ .meta-text {
+ font-size: 12px;
+ color: @grey-500;
+ }
+
+ .meta-separator {
+ color: @grey-400;
+ font-size: 12px;
+ }
+ }
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ArticleViewer.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ArticleViewer.component.tsx
new file mode 100644
index 000000000000..2ca8d81858db
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ArticleViewer.component.tsx
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { LinkOutlined } from '@ant-design/icons';
+import { Alert, Button, Spin } from 'antd';
+import { AxiosError } from 'axios';
+import React, { useCallback, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { LearningResource } from '../../../rest/learningResourceAPI';
+import { showErrorToast } from '../../../utils/ToastUtils';
+import RichTextEditorPreviewer from '../../common/RichTextEditor/RichTextEditorPreviewer';
+import './article-viewer.less';
+
+interface ArticleViewerProps {
+ resource: LearningResource;
+}
+
+export const ArticleViewer: React.FC = ({ resource }) => {
+ const { t } = useTranslation();
+ const [content, setContent] = useState('');
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const fetchContent = useCallback(async () => {
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ // Check for embedded content in embedConfig first
+ if (resource.source.embedConfig?.content) {
+ setContent(resource.source.embedConfig.content as string);
+ } else if (resource.source.url.startsWith('http')) {
+ const response = await fetch(resource.source.url);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch article: ${response.statusText}`);
+ }
+ const text = await response.text();
+ setContent(text);
+ } else {
+ setContent(resource.source.url);
+ }
+ } catch (err) {
+ const errorMessage =
+ err instanceof Error ? err.message : 'Failed to load article';
+ setError(errorMessage);
+ showErrorToast(err as AxiosError, t('server.article-fetch-error'));
+ } finally {
+ setIsLoading(false);
+ }
+ }, [resource.source.url, resource.source.embedConfig, t]);
+
+ useEffect(() => {
+ fetchContent();
+ }, [fetchContent]);
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
}
+ rel="noopener noreferrer"
+ size="small"
+ target="_blank"
+ type="link">
+ {t('label.open-original')}
+
+ }
+ description={error}
+ message={t('message.failed-to-load-article')}
+ type="error"
+ />
+
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ArticleViewer.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ArticleViewer.test.tsx
new file mode 100644
index 000000000000..0bfe4531c6d4
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ArticleViewer.test.tsx
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { render, screen, waitFor } from '@testing-library/react';
+import { LearningResource } from '../../../rest/learningResourceAPI';
+import { ArticleViewer } from './ArticleViewer.component';
+
+jest.mock('../../../utils/ToastUtils', () => ({
+ showErrorToast: jest.fn(),
+}));
+
+jest.mock('../../common/RichTextEditor/RichTextEditorPreviewer', () => {
+ return jest
+ .fn()
+ .mockImplementation(({ markdown }) => (
+ {markdown}
+ ));
+});
+
+const createMockResource = (
+ url: string,
+ embedContent?: string
+): LearningResource => ({
+ id: 'test-id',
+ name: 'test-article',
+ displayName: 'Test Article',
+ resourceType: 'Article',
+ source: {
+ url,
+ embedConfig: embedContent ? { content: embedContent } : undefined,
+ },
+ contexts: [{ pageId: 'glossary' }],
+});
+
+describe('ArticleViewer', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ global.fetch = jest.fn();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should render loading state initially', () => {
+ const resource = createMockResource('https://example.com/article.md');
+ (global.fetch as jest.Mock).mockImplementation(() => new Promise(() => {}));
+
+ render( );
+
+ expect(screen.getByText('label.loading-article')).toBeInTheDocument();
+ });
+
+ it('should render embedded content from embedConfig', async () => {
+ const embeddedContent = '# Test Article Content';
+ const resource = createMockResource('https://example.com', embeddedContent);
+
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('rich-text-previewer')).toBeInTheDocument();
+ });
+
+ expect(screen.getByText(embeddedContent)).toBeInTheDocument();
+ });
+
+ it('should fetch and render content from URL', async () => {
+ const resource = createMockResource('https://example.com/article.md');
+ const fetchedContent = '# Fetched Article';
+
+ (global.fetch as jest.Mock).mockResolvedValue({
+ ok: true,
+ text: () => Promise.resolve(fetchedContent),
+ });
+
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('rich-text-previewer')).toBeInTheDocument();
+ });
+
+ expect(screen.getByText(fetchedContent)).toBeInTheDocument();
+ });
+
+ it('should render error state when fetch fails', async () => {
+ const resource = createMockResource('https://example.com/article.md');
+
+ (global.fetch as jest.Mock).mockResolvedValue({
+ ok: false,
+ statusText: 'Not Found',
+ });
+
+ render( );
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('message.failed-to-load-article')
+ ).toBeInTheDocument();
+ });
+ });
+
+ it('should render error state when fetch throws', async () => {
+ const resource = createMockResource('https://example.com/article.md');
+
+ (global.fetch as jest.Mock).mockRejectedValue(new Error('Network error'));
+
+ render( );
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('message.failed-to-load-article')
+ ).toBeInTheDocument();
+ });
+ });
+
+ it('should render URL directly if not http URL and no embedConfig', async () => {
+ const resource = createMockResource('Some direct markdown content');
+
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('rich-text-previewer')).toBeInTheDocument();
+ });
+
+ expect(
+ screen.getByText('Some direct markdown content')
+ ).toBeInTheDocument();
+ });
+
+ it('should show open original link button on error', async () => {
+ const resource = createMockResource('https://example.com/article.md');
+
+ (global.fetch as jest.Mock).mockResolvedValue({
+ ok: false,
+ statusText: 'Not Found',
+ });
+
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByText('label.open-original')).toBeInTheDocument();
+ });
+ });
+
+ it('should prioritize embedConfig over URL fetch', async () => {
+ const embeddedContent = '# Embedded Content';
+ const resource = createMockResource(
+ 'https://example.com/article.md',
+ embeddedContent
+ );
+
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('rich-text-previewer')).toBeInTheDocument();
+ });
+
+ expect(global.fetch).not.toHaveBeenCalled();
+ expect(screen.getByText(embeddedContent)).toBeInTheDocument();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ResourcePlayerModal.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ResourcePlayerModal.component.tsx
new file mode 100644
index 000000000000..bffc05e4bf43
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ResourcePlayerModal.component.tsx
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ CloseOutlined,
+ ExpandAltOutlined,
+ ShrinkOutlined,
+} from '@ant-design/icons';
+import { Button, Modal, Tag, Typography } from 'antd';
+import { DateTime } from 'luxon';
+import React, { useCallback, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { LEARNING_CATEGORIES } from '../Learning.interface';
+import { ArticleViewer } from './ArticleViewer.component';
+import './resource-player-modal.less';
+import { ResourcePlayerModalProps } from './ResourcePlayerModal.interface';
+import { StorylaneTour } from './StorylaneTour.component';
+import { VideoPlayer } from './VideoPlayer.component';
+
+const { Link, Paragraph, Text } = Typography;
+
+const MAX_VISIBLE_TAGS = 3;
+
+export const ResourcePlayerModal: React.FC = ({
+ open,
+ resource,
+ onClose,
+}) => {
+ const { t } = useTranslation();
+ const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
+ const [isFullScreen, setIsFullScreen] = useState(false);
+ const [isDescriptionTruncated, setIsDescriptionTruncated] = useState(false);
+
+ const formattedDuration = useMemo(() => {
+ if (!resource.estimatedDuration) {
+ return null;
+ }
+ const minutes = Math.floor(resource.estimatedDuration / 60);
+
+ return `${minutes} ${t('label.min-watch')}`;
+ }, [resource.estimatedDuration, t]);
+
+ const formattedDate = useMemo(() => {
+ if (!resource.updatedAt) {
+ return null;
+ }
+
+ return DateTime.fromMillis(resource.updatedAt).toFormat('LLL d, yyyy');
+ }, [resource.updatedAt]);
+
+ const categoryTags = useMemo(() => {
+ if (!resource.categories || resource.categories.length === 0) {
+ return { visible: [], remaining: 0 };
+ }
+
+ const visible = resource.categories.slice(0, MAX_VISIBLE_TAGS);
+ const remaining = resource.categories.length - MAX_VISIBLE_TAGS;
+
+ return { visible, remaining };
+ }, [resource.categories]);
+
+ const getCategoryColors = useCallback((category: string) => {
+ const categoryInfo =
+ LEARNING_CATEGORIES[category as keyof typeof LEARNING_CATEGORIES];
+
+ return {
+ bgColor: categoryInfo?.bgColor ?? '#f8f9fc',
+ borderColor: categoryInfo?.borderColor ?? '#d5d9eb',
+ color: categoryInfo?.color ?? '#363f72',
+ };
+ }, []);
+
+ const handleViewMoreClick = useCallback(() => {
+ setIsDescriptionExpanded(!isDescriptionExpanded);
+ }, [isDescriptionExpanded]);
+
+ const handleFullScreenToggle = useCallback(() => {
+ setIsFullScreen((prev) => !prev);
+ }, []);
+
+ const renderPlayer = useMemo(() => {
+ switch (resource.resourceType) {
+ case 'Video':
+ return ;
+ case 'Storylane':
+ return ;
+ case 'Article':
+ return ;
+ default:
+ return {t('message.unsupported-resource-type')}
;
+ }
+ }, [resource, t]);
+
+ return (
+
+
+
+
+ {resource.displayName || resource.name}
+
+
+ {resource.description && (
+
+
+ {resource.description}
+
+ {(isDescriptionTruncated || isDescriptionExpanded) && (
+
+ {isDescriptionExpanded
+ ? t('label.view-less')
+ : t('label.view-more')}
+
+ )}
+
+ )}
+
+
+
+ {categoryTags.visible.map((category) => {
+ const colors = getCategoryColors(category);
+
+ return (
+
+ {LEARNING_CATEGORIES[
+ category as keyof typeof LEARNING_CATEGORIES
+ ]?.label ?? category}
+
+ );
+ })}
+ {categoryTags.remaining > 0 && (
+
+ +{categoryTags.remaining}
+
+ )}
+
+
+
+ {formattedDate && (
+ {formattedDate}
+ )}
+ {formattedDate && formattedDuration && (
+ |
+ )}
+ {formattedDuration && (
+ {formattedDuration}
+ )}
+
+
+
+
+
+
+ {isFullScreen ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ {renderPlayer}
+
+ );
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ResourcePlayerModal.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ResourcePlayerModal.interface.ts
new file mode 100644
index 000000000000..8534f3efe396
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ResourcePlayerModal.interface.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { LearningResource } from '../../../rest/learningResourceAPI';
+
+export interface ResourcePlayerModalProps {
+ open: boolean;
+ resource: LearningResource;
+ onClose: () => void;
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ResourcePlayerModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ResourcePlayerModal.test.tsx
new file mode 100644
index 000000000000..b4f12f74ff9f
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ResourcePlayerModal.test.tsx
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { fireEvent, render, screen } from '@testing-library/react';
+import { LearningResource } from '../../../rest/learningResourceAPI';
+import { ResourcePlayerModal } from './ResourcePlayerModal.component';
+
+jest.mock('./VideoPlayer.component', () => ({
+ VideoPlayer: jest
+ .fn()
+ .mockImplementation(() =>
),
+}));
+
+jest.mock('./StorylaneTour.component', () => ({
+ StorylaneTour: jest
+ .fn()
+ .mockImplementation(() =>
),
+}));
+
+jest.mock('./ArticleViewer.component', () => ({
+ ArticleViewer: jest
+ .fn()
+ .mockImplementation(() =>
),
+}));
+
+const mockOnClose = jest.fn();
+
+const createMockResource = (
+ resourceType: 'Video' | 'Storylane' | 'Article',
+ overrides?: Partial
+): LearningResource => ({
+ id: 'test-id',
+ name: 'test-resource',
+ displayName: 'Test Resource',
+ description: 'This is a test resource description',
+ resourceType,
+ source: { url: 'https://example.com/resource' },
+ contexts: [{ pageId: 'glossary' }],
+ categories: ['Discovery', 'DataGovernance'],
+ estimatedDuration: 300,
+ updatedAt: 1704067200000,
+ ...overrides,
+});
+
+describe('ResourcePlayerModal', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render modal when open is true', () => {
+ const resource = createMockResource('Video');
+ render(
+
+ );
+
+ expect(screen.getByText('Test Resource')).toBeInTheDocument();
+ });
+
+ it('should not render modal content when open is false', () => {
+ const resource = createMockResource('Video');
+ render(
+
+ );
+
+ expect(screen.queryByText('Test Resource')).not.toBeInTheDocument();
+ });
+
+ it('should render VideoPlayer for Video resource type', () => {
+ const resource = createMockResource('Video');
+ render(
+
+ );
+
+ expect(screen.getByTestId('video-player')).toBeInTheDocument();
+ });
+
+ it('should render StorylaneTour for Storylane resource type', () => {
+ const resource = createMockResource('Storylane');
+ render(
+
+ );
+
+ expect(screen.getByTestId('storylane-tour')).toBeInTheDocument();
+ });
+
+ it('should render ArticleViewer for Article resource type', () => {
+ const resource = createMockResource('Article');
+ render(
+
+ );
+
+ expect(screen.getByTestId('article-viewer')).toBeInTheDocument();
+ });
+
+ it('should display resource description', () => {
+ const resource = createMockResource('Video');
+ render(
+
+ );
+
+ expect(
+ screen.getByLabelText('This is a test resource description')
+ ).toBeInTheDocument();
+ });
+
+ it('should display category tags', () => {
+ const resource = createMockResource('Video');
+ render(
+
+ );
+
+ expect(screen.getByText('Discovery')).toBeInTheDocument();
+ expect(screen.getByText('Governance')).toBeInTheDocument();
+ });
+
+ it('should display formatted duration', () => {
+ const resource = createMockResource('Video');
+ render(
+
+ );
+
+ expect(screen.getByText('5 label.min-watch')).toBeInTheDocument();
+ });
+
+ it('should call onClose when close button is clicked', () => {
+ const resource = createMockResource('Video');
+ render(
+
+ );
+
+ const closeButton = screen.getByRole('button', { name: /close/i });
+ fireEvent.click(closeButton);
+
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+
+ it('should use resource name as fallback when displayName is not provided', () => {
+ const resource = createMockResource('Video', {
+ displayName: undefined,
+ });
+ render(
+
+ );
+
+ expect(screen.getByText('test-resource')).toBeInTheDocument();
+ });
+
+ it('should show +N tag when more than 3 categories', () => {
+ const resource = createMockResource('Video', {
+ categories: [
+ 'Discovery',
+ 'DataGovernance',
+ 'Observability',
+ 'DataQuality',
+ 'Lineage',
+ ],
+ });
+ render(
+
+ );
+
+ expect(screen.getByText('+2')).toBeInTheDocument();
+ });
+
+ it('should not display duration when not provided', () => {
+ const resource = createMockResource('Video', {
+ estimatedDuration: undefined,
+ });
+ render(
+
+ );
+
+ expect(screen.queryByText(/label.min-watch/)).not.toBeInTheDocument();
+ });
+
+ it('should not display description section when description is not provided', () => {
+ const resource = createMockResource('Video', {
+ description: undefined,
+ });
+ render(
+
+ );
+
+ expect(
+ screen.queryByLabelText(/test resource description/i)
+ ).not.toBeInTheDocument();
+ });
+
+ it('should render description with ellipsis configuration when description is provided', () => {
+ const resource = createMockResource('Video', {
+ description: 'A very long description that should be truncated initially',
+ });
+ render(
+
+ );
+
+ const descriptionElement = screen.getByLabelText(
+ /A very long description that should be truncated initially/i
+ );
+
+ expect(descriptionElement).toBeInTheDocument();
+ expect(descriptionElement).toHaveClass('resource-description');
+ });
+
+ it('should display unsupported message for unknown resource type', () => {
+ const resource = createMockResource('Video', {
+ resourceType: 'Unknown' as 'Video',
+ });
+ render(
+
+ );
+
+ expect(
+ screen.getByText('message.unsupported-resource-type')
+ ).toBeInTheDocument();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/StorylaneTour.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/StorylaneTour.component.tsx
new file mode 100644
index 000000000000..4a21753605b3
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/StorylaneTour.component.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Spin } from 'antd';
+import React, { useCallback, useState } from 'react';
+import { LearningResource } from '../../../rest/learningResourceAPI';
+import './storylane-tour.less';
+
+interface StorylaneTourProps {
+ resource: LearningResource;
+}
+
+export const StorylaneTour: React.FC = ({ resource }) => {
+ const [isLoading, setIsLoading] = useState(true);
+
+ const handleLoad = useCallback(() => {
+ setIsLoading(false);
+ }, []);
+
+ return (
+
+
+ {isLoading && (
+
+
+
+ )}
+
+
+
+ );
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/StorylaneTour.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/StorylaneTour.test.tsx
new file mode 100644
index 000000000000..fbf335f4e990
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/StorylaneTour.test.tsx
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { fireEvent, render, screen } from '@testing-library/react';
+import { LearningResource } from '../../../rest/learningResourceAPI';
+import { StorylaneTour } from './StorylaneTour.component';
+
+const createMockResource = (url: string): LearningResource => ({
+ id: 'test-id',
+ name: 'test-storylane',
+ displayName: 'Test Storylane Tour',
+ resourceType: 'Storylane',
+ source: { url },
+ contexts: [{ pageId: 'glossary' }],
+});
+
+describe('StorylaneTour', () => {
+ it('should render loading spinner initially', () => {
+ const resource = createMockResource('https://app.storylane.io/demo/abc123');
+ render( );
+
+ expect(document.querySelector('.ant-spin')).toBeInTheDocument();
+ });
+
+ it('should render iframe with correct URL', () => {
+ const resource = createMockResource('https://app.storylane.io/demo/abc123');
+ render( );
+
+ const iframe = screen.getByTitle('Test Storylane Tour');
+
+ expect(iframe).toBeInTheDocument();
+ expect(iframe.getAttribute('src')).toBe(
+ 'https://app.storylane.io/demo/abc123'
+ );
+ });
+
+ it('should have sandbox attribute for security', () => {
+ const resource = createMockResource('https://app.storylane.io/demo/abc123');
+ render( );
+
+ const iframe = screen.getByTitle('Test Storylane Tour');
+
+ expect(iframe.getAttribute('sandbox')).toBe(
+ 'allow-scripts allow-same-origin allow-presentation allow-popups allow-forms'
+ );
+ });
+
+ it('should hide loading spinner after iframe loads', () => {
+ const resource = createMockResource('https://app.storylane.io/demo/abc123');
+ render( );
+
+ const iframe = screen.getByTitle('Test Storylane Tour');
+ fireEvent.load(iframe);
+
+ expect(document.querySelector('.ant-spin')).not.toBeInTheDocument();
+ });
+
+ it('should use resource name as title fallback', () => {
+ const resource: LearningResource = {
+ id: 'test-id',
+ name: 'storylane-name',
+ resourceType: 'Storylane',
+ source: { url: 'https://app.storylane.io/demo/abc123' },
+ contexts: [{ pageId: 'glossary' }],
+ };
+ render( );
+
+ expect(screen.getByTitle('storylane-name')).toBeInTheDocument();
+ });
+
+ it('should have allowFullScreen attribute', () => {
+ const resource = createMockResource('https://app.storylane.io/demo/abc123');
+ render( );
+
+ const iframe = screen.getByTitle('Test Storylane Tour');
+
+ expect(iframe).toHaveAttribute('allowfullscreen');
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/VideoPlayer.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/VideoPlayer.component.tsx
new file mode 100644
index 000000000000..f5d114af2ef6
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/VideoPlayer.component.tsx
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Spin } from 'antd';
+import React, { useCallback, useMemo, useState } from 'react';
+import { LearningResource } from '../../../rest/learningResourceAPI';
+import './video-player.less';
+
+interface VideoPlayerProps {
+ resource: LearningResource;
+}
+
+export const VideoPlayer: React.FC = ({ resource }) => {
+ const [isLoading, setIsLoading] = useState(true);
+
+ const embedUrl = useMemo(() => {
+ const url = resource.source.url;
+
+ let parsedUrl: URL | undefined;
+ try {
+ parsedUrl = new URL(url);
+ } catch {
+ // If URL parsing fails, fall back to the original URL.
+ return url;
+ }
+
+ const hostname = parsedUrl.hostname.toLowerCase();
+
+ const isYouTubeHost =
+ hostname === 'youtube.com' ||
+ hostname === 'www.youtube.com' ||
+ hostname === 'm.youtube.com' ||
+ hostname === 'youtu.be';
+
+ if (isYouTubeHost) {
+ // Handle different YouTube URL formats
+ if (hostname === 'youtu.be') {
+ // Short URL: https://youtu.be/?...
+ const pathParts = parsedUrl.pathname.split('/').filter(Boolean);
+ const videoId = pathParts[0] || '';
+ if (videoId) {
+ return `https://www.youtube.com/embed/${videoId}?enablejsapi=1&origin=${window.location.origin}`;
+ }
+ } else if (parsedUrl.pathname.startsWith('/watch')) {
+ // Watch URL: https://www.youtube.com/watch?v=&...
+ const videoId = parsedUrl.searchParams.get('v') || '';
+ if (videoId) {
+ return `https://www.youtube.com/embed/${videoId}?enablejsapi=1&origin=${window.location.origin}`;
+ }
+ } else if (parsedUrl.pathname.startsWith('/embed/')) {
+ // Already an embed URL; preserve as-is.
+ return url;
+ }
+ }
+
+ const isVimeoHost =
+ hostname === 'vimeo.com' ||
+ hostname === 'www.vimeo.com' ||
+ hostname === 'player.vimeo.com';
+
+ if (isVimeoHost) {
+ // Vimeo URL: https://vimeo.com/ or https://player.vimeo.com/video/
+ const pathParts = parsedUrl.pathname.split('/').filter(Boolean);
+ // For both vimeo.com/ and player.vimeo.com/video/, the last path segment is typically the video ID.
+ const videoId = pathParts[pathParts.length - 1] || '';
+
+ if (videoId) {
+ return `https://player.vimeo.com/video/${videoId}`;
+ }
+ }
+
+ return url;
+ }, [resource.source.url]);
+
+ const handleLoad = useCallback(() => {
+ setIsLoading(false);
+ }, []);
+
+ return (
+
+
+ {isLoading && (
+
+
+
+ )}
+
+
+
+ );
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/VideoPlayer.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/VideoPlayer.test.tsx
new file mode 100644
index 000000000000..73b02af72acb
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/VideoPlayer.test.tsx
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { fireEvent, render, screen } from '@testing-library/react';
+import { LearningResource } from '../../../rest/learningResourceAPI';
+import { VideoPlayer } from './VideoPlayer.component';
+
+const createMockResource = (url: string): LearningResource => ({
+ id: 'test-id',
+ name: 'test-video',
+ displayName: 'Test Video',
+ resourceType: 'Video',
+ source: { url },
+ contexts: [{ pageId: 'glossary' }],
+});
+
+describe('VideoPlayer', () => {
+ it('should render loading spinner initially', () => {
+ const resource = createMockResource(
+ 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
+ );
+ render( );
+
+ expect(document.querySelector('.ant-spin')).toBeInTheDocument();
+ });
+
+ it('should render iframe with correct YouTube embed URL', () => {
+ const resource = createMockResource(
+ 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
+ );
+ render( );
+
+ const iframe = screen.getByTitle('Test Video');
+
+ expect(iframe).toBeInTheDocument();
+ expect(iframe.getAttribute('src')).toContain(
+ 'https://www.youtube.com/embed/dQw4w9WgXcQ'
+ );
+ });
+
+ it('should handle YouTube short URL format', () => {
+ const resource = createMockResource('https://youtu.be/dQw4w9WgXcQ');
+ render( );
+
+ const iframe = screen.getByTitle('Test Video');
+
+ expect(iframe.getAttribute('src')).toContain(
+ 'https://www.youtube.com/embed/dQw4w9WgXcQ'
+ );
+ });
+
+ it('should preserve YouTube embed URL as-is', () => {
+ const resource = createMockResource(
+ 'https://www.youtube.com/embed/dQw4w9WgXcQ'
+ );
+ render( );
+
+ const iframe = screen.getByTitle('Test Video');
+
+ expect(iframe.getAttribute('src')).toBe(
+ 'https://www.youtube.com/embed/dQw4w9WgXcQ'
+ );
+ });
+
+ it('should handle Vimeo URL format', () => {
+ const resource = createMockResource('https://vimeo.com/123456789');
+ render( );
+
+ const iframe = screen.getByTitle('Test Video');
+
+ expect(iframe.getAttribute('src')).toBe(
+ 'https://player.vimeo.com/video/123456789'
+ );
+ });
+
+ it('should handle Vimeo player URL format', () => {
+ const resource = createMockResource(
+ 'https://player.vimeo.com/video/123456789'
+ );
+ render( );
+
+ const iframe = screen.getByTitle('Test Video');
+
+ expect(iframe.getAttribute('src')).toBe(
+ 'https://player.vimeo.com/video/123456789'
+ );
+ });
+
+ it('should use original URL for unknown video providers', () => {
+ const resource = createMockResource('https://example.com/video.mp4');
+ render( );
+
+ const iframe = screen.getByTitle('Test Video');
+
+ expect(iframe.getAttribute('src')).toBe('https://example.com/video.mp4');
+ });
+
+ it('should have sandbox attribute for security', () => {
+ const resource = createMockResource(
+ 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
+ );
+ render( );
+
+ const iframe = screen.getByTitle('Test Video');
+
+ expect(iframe.getAttribute('sandbox')).toBe(
+ 'allow-scripts allow-same-origin allow-presentation allow-popups'
+ );
+ });
+
+ it('should hide loading spinner after iframe loads', () => {
+ const resource = createMockResource(
+ 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
+ );
+ render( );
+
+ const iframe = screen.getByTitle('Test Video');
+ fireEvent.load(iframe);
+
+ expect(document.querySelector('.ant-spin')).not.toBeInTheDocument();
+ });
+
+ it('should use resource name as title fallback', () => {
+ const resource: LearningResource = {
+ id: 'test-id',
+ name: 'test-video-name',
+ resourceType: 'Video',
+ source: { url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' },
+ contexts: [{ pageId: 'glossary' }],
+ };
+ render( );
+
+ expect(screen.getByTitle('test-video-name')).toBeInTheDocument();
+ });
+
+ it('should handle invalid URL gracefully', () => {
+ const resource = createMockResource('not-a-valid-url');
+ render( );
+
+ const iframe = screen.getByTitle('Test Video');
+
+ expect(iframe.getAttribute('src')).toBe('not-a-valid-url');
+ });
+
+ it('should reject URLs with youtube.com in path but different host', () => {
+ const resource = createMockResource(
+ 'https://malicious-site.com/youtube.com/watch?v=abc123'
+ );
+ render( );
+
+ const iframe = screen.getByTitle('Test Video');
+
+ expect(iframe.getAttribute('src')).toBe(
+ 'https://malicious-site.com/youtube.com/watch?v=abc123'
+ );
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/article-viewer.less b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/article-viewer.less
new file mode 100644
index 000000000000..bb5775074726
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/article-viewer.less
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.article-viewer-wrapper {
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ padding: 24px 48px;
+}
+
+.article-viewer-container {
+ width: 100%;
+ max-width: 1200px;
+ max-height: 532.6875px;
+ overflow-y: auto;
+ background-color: #fff;
+ border-radius: 8px;
+ padding: 24px;
+
+ .article-viewer-content {
+ max-width: 800px;
+ margin: 0 auto;
+ }
+}
+
+.article-viewer-loading,
+.article-viewer-error {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ max-width: 1200px;
+ min-height: 400px;
+ background-color: #fff;
+ border-radius: 8px;
+ padding: 24px;
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/resource-player-modal.less b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/resource-player-modal.less
new file mode 100644
index 000000000000..24a3cbc0f6c4
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/resource-player-modal.less
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import (reference) '../../../styles/variables.less';
+
+.resource-player-modal {
+ .ant-modal-content {
+ padding: 0;
+ overflow: hidden;
+ border-radius: @border-rad-sm;
+ background-color: @grey-100;
+ }
+
+ .ant-modal-body {
+ padding: 0;
+ }
+
+ .resource-player-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: @size-md;
+ padding: @padding-md @padding-lg;
+ background-color: @white;
+ border-bottom: 1px solid @border-color-1;
+ }
+
+ .resource-player-info {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .resource-title {
+ font-size: 16px;
+ line-height: 24px;
+ color: @grey-900;
+ display: block;
+ margin-bottom: @margin-xs;
+ }
+
+ .resource-description-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: @size-xxs;
+ align-items: baseline;
+ margin-bottom: @margin-xs;
+ }
+
+ .resource-description {
+ margin: 0;
+ font-size: 13px;
+ line-height: 20px;
+ color: @grey-600;
+
+ &.collapsed {
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ }
+ }
+
+ .view-more-link {
+ font-size: 13px;
+ color: @primary-color;
+ cursor: pointer;
+ flex-shrink: 0;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ .resource-meta-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: @size-xs;
+ }
+
+ .resource-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: @size-xxs;
+
+ .category-tag {
+ margin: 0;
+ font-size: 11px;
+ line-height: 16px;
+ padding: 2px 6px;
+ border-radius: @border-rad-base;
+ font-weight: @font-medium;
+
+ &.more-tag {
+ background-color: @grey-100;
+ border-color: @grey-300;
+ color: @grey-600;
+ }
+ }
+ }
+
+ .resource-meta {
+ display: flex;
+ align-items: center;
+ gap: @size-xxs;
+ flex-shrink: 0;
+
+ .meta-text {
+ font-size: 12px;
+ color: @grey-500;
+ }
+
+ .meta-separator {
+ color: @grey-400;
+ font-size: 12px;
+ }
+ }
+
+ .resource-player-actions {
+ display: flex;
+ align-items: center;
+ gap: @size-sm;
+ flex-shrink: 0;
+
+ .action-icon {
+ font-size: 16px;
+ color: @grey-500;
+ cursor: pointer;
+ padding: @padding-xs;
+ border-radius: @border-rad-base;
+ transition: all 0.2s ease;
+
+ &:hover {
+ color: @grey-700;
+ background-color: @grey-100;
+ }
+ }
+ }
+
+ .resource-player-content {
+ width: 100%;
+ background-color: @grey-100;
+ }
+}
+
+.resource-player-modal.fullscreen {
+ max-width: 100vw !important;
+ padding: 0 !important;
+ top: 0 !important;
+ margin: 0 !important;
+
+ .ant-modal-content {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ border-radius: 0;
+ }
+
+ .ant-modal-body {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ }
+
+ .resource-player-header {
+ flex-shrink: 0;
+ }
+
+ .resource-player-content {
+ flex: 1;
+ min-height: 0;
+
+ .video-player-wrapper,
+ .storylane-tour-wrapper,
+ .article-viewer-wrapper {
+ width: 100%;
+ height: 100%;
+ padding: 16px;
+ }
+
+ .video-player-container,
+ .storylane-tour-container {
+ max-width: none;
+ max-height: none;
+ aspect-ratio: unset;
+ width: 100%;
+ height: 100%;
+ }
+
+ .article-viewer-container {
+ max-width: none;
+ max-height: none;
+ height: 100%;
+ }
+ }
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/storylane-tour.less b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/storylane-tour.less
new file mode 100644
index 000000000000..6017b536f386
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/storylane-tour.less
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.storylane-tour-wrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 24px 48px;
+}
+
+.storylane-tour-container {
+ position: relative;
+ width: 100%;
+ max-width: 1200px;
+ aspect-ratio: 16 / 9;
+ max-height: 532.6875px;
+ border-radius: 8px;
+ overflow: hidden;
+
+ .storylane-tour-iframe {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border: none;
+ }
+
+ .storylane-tour-loading {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 1;
+ }
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/video-player.less b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/video-player.less
new file mode 100644
index 000000000000..2124952c327d
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/video-player.less
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.video-player-wrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 24px 48px;
+}
+
+.video-player-container {
+ position: relative;
+ width: 100%;
+ max-width: 1200px;
+ aspect-ratio: 16 / 9;
+ max-height: 532.6875px;
+ border-radius: 8px;
+ overflow: hidden;
+
+ .video-player-iframe {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border: none;
+ }
+
+ .video-player-loading {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 1;
+ }
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/PageHeader/PageHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/PageHeader/PageHeader.component.tsx
index 422f77a6d442..6ce1950064ed 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/PageHeader/PageHeader.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/PageHeader/PageHeader.component.tsx
@@ -11,8 +11,9 @@
* limitations under the License.
*/
-import { Badge, Typography } from 'antd';
+import { Badge, Space, Typography } from 'antd';
import { useTranslation } from 'react-i18next';
+import { LearningIcon } from '../Learning/LearningIcon/LearningIcon.component';
import './page-header.less';
import { HeaderProps } from './PageHeader.interface';
@@ -21,27 +22,31 @@ const PageHeader = ({
titleProps,
subHeaderProps,
isBeta,
+ learningPageId,
}: HeaderProps) => {
const { t } = useTranslation();
return (
-
- {header}
+
+
+ {header}
- {isBeta && (
-
- )}
-
+ {isBeta && (
+
+ )}
+
+ {learningPageId &&
}
+
{
gutter={[16, 16]}>
-
+
{isFetchingStatus ? (
) : (
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/navigation/usePageHeader.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/navigation/usePageHeader.tsx
index 42b5a289bb23..0b4e2d4947d5 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/navigation/usePageHeader.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/navigation/usePageHeader.tsx
@@ -16,6 +16,7 @@ import { useTheme } from '@mui/material/styles';
import { Plus } from '@untitledui/icons';
import { ReactNode, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
+import { LearningIcon } from '../../../Learning/LearningIcon/LearningIcon.component';
interface PageHeaderConfig {
titleKey: string;
@@ -25,6 +26,7 @@ interface PageHeaderConfig {
addButtonLabelKey?: string;
addButtonTestId?: string;
onAddClick?: () => void;
+ learningPageId?: string;
}
export const usePageHeader = (config: PageHeaderConfig) => {
@@ -55,16 +57,21 @@ export const usePageHeader = (config: PageHeaderConfig) => {
alignItems: 'center',
}}>
-
- {displayTitle}
-
+
+
+ {displayTitle}
+
+ {config.learningPageId && (
+
+ )}
+
{displayDescription && (
{{entity}}0>، ولكنه فشل في النشر",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "فشل تحميل الصورة",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "فشل {{failed}} من أصل {{count}} عملية تحقق",
"feed-asset-action-header": "{{action}} <0>أصل بيانات0>",
"feed-custom-property-header": "تم تحديث الخصائص المخصصة على",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "ملاحظة: لم يتم تحقيق هدف مؤشر أداء الوصف بعد، ولكن لا يزال هناك وقت – بقي {{count}} يومًا لمؤسستك. للبقاء على المسار الصحيح، يرجى تفعيل تقرير رؤى البيانات. سيسمح لنا هذا بإرسال تحديثات أسبوعية إلى جميع الفرق، مما يعزز التعاون والتركيز نحو تحقيق مؤشرات الأداء الرئيسية لمؤسستنا.",
"latency-sla-description": "<0>{{label}}0>: يجب أن تكون استجابة الاستعلام أقل من <0>{{data}}0>",
"latest-offset-description": "أحدث إزاحة للحدث في النظام.",
+ "learning-resources-management-description": "استكشف ميزات المنتج وتعلم كيفية عملها من خلال مواردنا",
"leave-the-team-team-name": "مغادرة الفريق {{teamName}}",
"length-validator-error": "مطلوب ما لا يقل عن {{length}} {{field}}",
"lineage-ingestion-description": "يمكن تكوين ونشر سير عمل سلسلة البيانات بعد إعداد استيعاب البيانات الوصفية. يحصل سير عمل استيعاب سلسلة البيانات على سجل الاستعلام ويحلل استعلامات CREATE، INSERT، MERGE... ويجهز سلسلة البيانات بين الكيانات المعنية. يمكن أن يحتوي استيعاب سلسلة البيانات على سير عمل واحد فقط لخدمة قاعدة بيانات. حدد مدة سجل الاستعلام (بالأيام) وحد النتائج للبدء.",
@@ -2512,7 +2536,8 @@
"no-kpi": "حدد مؤشرات الأداء الرئيسية لمراقبة التأثير واتخاذ قرارات أكثر ذكاءً.",
"no-kpi-available-add-new-one": "لا تتوفر مؤشرات أداء رئيسية. انقر على زر إضافة مؤشر أداء رئيسي لإضافة واحد.",
"no-kpi-found": "لم يتم العثور على مؤشر أداء رئيسي بالاسم {{name}}",
- "no-lineage-available": "لم يتم العثور على اتصالات سلسلة البيانات",
+ "no-learning-resources-available": "لا توجد موارد تعليمية متاحة لهذا السياق.",
+ "no-lineage-available": "No lineage connections found",
"no-match-found": "لم يتم العثور على تطابق",
"no-mentions": "يبدو أنك وفريقك واضحون تمامًا - لا توجد إشارات في أي أنشطة بعد. استمروا في العمل الرائع!",
"no-notification-found": "لم يتم العثور على إشعارات",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "يمكن للمراجعين فقط الموافقة أو الرفض",
"only-show-columns-with-lineage": "إظهار الأعمدة ذات سلسلة البيانات فقط",
"optional-configuration-update-description-dbt": "تكوين اختياري لتحديث الوصف من dbt أو لا",
+ "optional-markdown-content": "Optional: Add markdown content to embed directly in the resource",
"owners-coverage-widget-description": "تغطية المالكين لجميع أصول البيانات في الخدمة. <0>تعرف على المزيد.0>",
"page-is-not-available": "الصفحة التي تبحث عنها غير متاحة",
"page-sub-header-for-activity-feed": "ملخص النشاط الذي يمكّنك من عرض ملخص لأحداث تغيير البيانات.",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "تجاهل",
"unsaved-changes-save": "حفظ التغييرات",
"unsaved-changes-warning": "قد تكون لديك تغييرات غير محفوظة سيتم تجاهلها عند الإغلاق.",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "طلب تحديث الوصف لـ",
"update-displayName-entity": "تحديث اسم العرض لـ {{entity}}.",
"update-profiler-settings": "تحديث إعدادات المُحلل (profiler).",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "مرحبًا بك في OpenMetadata!",
"welcome-to-open-metadata": "مرحبًا بك في OpenMetadata!",
"would-like-to-start-adding-some": "هل ترغب في البدء بإضافة البعض؟",
+ "write-markdown-content": "Write markdown content here...",
"write-your-announcement-lowercase": "اكتب إعلانك",
"write-your-description": "اكتب وصفك",
"write-your-text": "اكتب {{text}} الخاص بك",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "تم التحقق من البريد الإلكتروني بنجاح",
"add-entity-error": "خطأ أثناء إضافة {{entity}}!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "مزود المصادقة {{provider}} غير مدعوم لتجديد الرموز المميزة.",
"can-not-renew-token-authentication-not-present": "لا يمكن تجديد رمز الهوية المميز. مزود المصادقة غير موجود.",
"column-fetch-error": "خطأ أثناء جلب حالة اختبار العمود!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "لقد أدخلت اسم مستخدم أو كلمة مرور غير صالحة.",
"join-team-error": "خطأ أثناء الانضمام إلى الفريق!",
"join-team-success": "تم الانضمام إلى الفريق بنجاح!",
+ "learning-resources-fetch-error": "خطأ في جلب الموارد التعليمية",
"leave-team-error": "خطأ أثناء مغادرة الفريق!",
"leave-team-success": "تم مغادرة الفريق بنجاح!",
"no-application-schema-found": "لم يتم العثور على مخطط تطبيق لـ {{appName}}",
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 62e686862f16..9ed2ede6a60d 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
@@ -59,6 +59,7 @@
"add-new-field": "Neues Feld hinzufügen",
"add-new-widget-plural": "Neue Widgets hinzufügen",
"add-product": "Produkt hinzufügen",
+ "add-resource": "Add Resource",
"add-row": "Zeile hinzufügen",
"add-suggestion": "Vorschlag hinzufügen",
"add-term-boost": "Begriffsverstärkung hinzufügen",
@@ -315,6 +316,8 @@
"container": "Container",
"container-column": "Container-Spalte",
"container-plural": "Container",
+ "content-name": "Content Name",
+ "context": "Kontext",
"contract": "Vertrag",
"contract-detail-plural": "Vertragsdetails",
"contract-execution-history": "Vertragsausführungsverlauf",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "Glossarnamen bearbeiten",
"edit-glossary-name": "Glossarnamen bearbeiten",
"edit-name": "Namen bearbeiten",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "Vorschlag bearbeiten",
"edit-widget": "Widget bearbeiten",
"edit-widget-plural": "Widgets bearbeiten",
@@ -597,6 +601,7 @@
"embed-file-type": "Embed {{fileType}}",
"embed-image": "Bild einbetten",
"embed-link": "Link einbetten",
+ "embedded-content": "Eingebetteter Inhalt",
"enable": "Aktivieren",
"enable-debug-log": "Debug-Protokoll aktivieren",
"enable-entity": "Enable {{entity}}",
@@ -657,6 +662,7 @@
"entity-reference-plural": "Entität Referenz",
"entity-reference-types": "Entität Referenz Typ",
"entity-report-data": "Entitätsberichtsdaten",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}} läuft",
"entity-scheduled-to-run-value": "{{entity}} geplant zur Ausführung {{value}}",
"entity-service": "{{entity}}-Dienst",
@@ -995,8 +1001,11 @@
"latest-offset": "Neuester Offset",
"layer": "Layer",
"layer-plural": "Layers",
+ "learn-how-this-feature-works": "Erfahren Sie, wie diese Funktion funktioniert?",
"learn-more": "Mehr erfahren",
"learn-more-and-support": "Mehr erfahren & Hilfe",
+ "learning-resource": "Lernressource",
+ "learning-resources": "Lernressourcen",
"leave-team": "Team verlassen",
"legend": "Legend",
"less": "Weniger",
@@ -1025,7 +1034,8 @@
"live": "Live",
"load-more": "Mehr laden",
"loading": "Laden",
- "loading-graph": "Grafik wird geladen",
+ "loading-article": "Loading article...",
+ "loading-graph": "Loading Graph",
"local-config-source": "Lokale Konfigurationsquelle",
"location": "Standort",
"log-lowercase-plural": "logs",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "MIME-Typ",
"min": "Minimal",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "Mindmap",
"minor": "Minderjährig",
"minute": "Minute",
@@ -1123,6 +1135,7 @@
"month": "Monat",
"monthly": "Monatlich",
"more": "Mehr",
+ "more-action": "More Action",
"more-help": "Mehr Hilfe",
"more-lowercase": "mehr",
"most-active-user": "Aktivste Benutzer",
@@ -1244,6 +1257,7 @@
"open-metadata": "OpenMetadata",
"open-metadata-logo": "OpenMetadata-Logo",
"open-metadata-url": "OpenMetadata-URL",
+ "open-original": "Open Original",
"open-task-plural": "Aufgaben öffnen",
"operation": "Operation",
"operation-plural": "Operationen",
@@ -1273,6 +1287,7 @@
"ownership": "Eigentum",
"page": "Seite",
"page-not-found": "Seite nicht gefunden",
+ "page-plural": "Seiten",
"page-views-by-data-asset-plural": "Seitenaufrufe nach Datenanlage",
"parameter": "Parameter",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "Spalten zum Ausschließen auswählen",
"select-column-plural-to-include": "Spalten zum Einschließen auswählen",
"select-dimension": "Dimension auswählen",
+ "select-duration": "Select Duration",
"select-entity": "{{entity}} auswählen",
"select-entity-type": "Entitätstyp auswählen",
"select-field": "{{field}}-Feld auswählen",
"select-field-and-weight": "Feld und Gewicht auswählen",
+ "select-page-plural": "Seiten auswählen",
"select-schema-object": "Schemaobjekt auswählen",
+ "select-status": "Select Status",
"select-tag": "Eine Tag auswählen",
"select-test-type": "Testtyp auswählen",
"select-to-search": "Zum Suchen auswählen",
@@ -1675,6 +1693,7 @@
"source-label": "Quelle:",
"source-match": "Quellenübereinstimmung",
"source-plural": "Quellen",
+ "source-provider": "Quellenanbieter",
"source-url": "Quell-URL",
"source-with-details": "Quelle: {{source}} ({{entityName}})",
"specific-data-asset-plural": "Spezifische Datenbestände",
@@ -1916,6 +1935,7 @@
"update-image": "Bild aktualisieren",
"update-request-tag-plural": "Anforderung von Tags aktualisieren",
"updated": "Aktualisiert",
+ "updated-at": "Updated at",
"updated-by": "Aktualisiert von",
"updated-lowercase": "aktualisiert",
"updated-on": "Aktualisiert am",
@@ -2260,6 +2280,7 @@
"enter-a-field": "Geben Sie ein {{field}} ein.",
"enter-column-description": "Geben Sie eine Spaltenbeschreibung ein.",
"enter-comma-separated-field": "Geben Sie {{field}} durch Kommas getrennt ein.",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "Geben Sie einen Anzeigenamen ein.",
"enter-feature-description": "Geben Sie eine Funktionserklärung ein.",
"enter-interval": "Geben Sie ein Intervall ein.",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "Es können nur externe Ziele getestet werden.",
"failed-events-description": "Anzahl fehlgeschlagener Ereignisse für einen bestimmten Alert.",
"failed-status-for-entity-deploy": "<0>{{entity}}0> wurde {{entityStatus}}, aber das Bereitstellen ist fehlgeschlagen.",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "Bild konnte nicht geladen werden",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{failed}} fehlgeschlagen von {{count}} Prüfungen",
"feed-asset-action-header": "{{action}} <0>data asset0>",
"feed-custom-property-header": "updated Custom Properties on",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "Macht nichts. Es ist Zeit, Ihre Ziele neu zu strukturieren und schneller voranzukommen.",
"latency-sla-description": "<0>{{label}}0}: Die Antwortzeit der Abfrage muss unter <0>{{data}}0> liegen.",
"latest-offset-description": "Der neueste Offset des Ereignisses im System.",
+ "learning-resources-management-description": "Erkunden Sie Produktfunktionen und erfahren Sie, wie sie funktionieren, durch unsere Ressourcen",
"leave-the-team-team-name": "Verlassen Sie das Team {{teamName}}",
"length-validator-error": "Mindestens {{length}} {{field}} erforderlich.",
"lineage-ingestion-description": "Die Verbindungsdaten-Erfassung kann konfiguriert und bereitgestellt werden, nachdem eine Metadaten-Erfassung eingerichtet wurde. Der Verbindungsdaten-Erfassungsworkflow ruft den Abfrageverlauf ab, analysiert Abfragen wie CREATE, INSERT, MERGE usw. und bereitet die Verbindung zwischen den beteiligten Entitäten vor. Die Verbindungsdaten-Erfassung kann nur eine Pipeline für einen Datenbankdienst haben. Definieren Sie die Dauer des Abfrageprotokolls (in Tagen) und die Ergebnisgrenze, um zu starten.",
@@ -2512,6 +2536,7 @@
"no-kpi": "Definieren Sie Schlüsselkennzahlen, um Auswirkungen zu überwachen und fundiertere Entscheidungen zu treffen.",
"no-kpi-available-add-new-one": "Keine KPIs verfügbar. Klicken Sie auf die Schaltfläche 'KPI hinzufügen', um einen hinzuzufügen.",
"no-kpi-found": "Kein KPI mit dem Namen {{name}} gefunden",
+ "no-learning-resources-available": "Keine Lernressourcen für diesen Kontext verfügbar.",
"no-lineage-available": "Keine Lineage-Verbindungen gefunden",
"no-match-found": "Keine Übereinstimmung gefunden",
"no-mentions": "Es gibt keine Instanzen, in denen Sie oder Ihr Team in Aktivitäten erwähnt wurden",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "Nur Prüfer können genehmigen oder ablehnen.",
"only-show-columns-with-lineage": "Nur Spalten mit Lineage anzeigen",
"optional-configuration-update-description-dbt": "Optionale Konfiguration, um die Beschreibung von dbt zu aktualisieren oder nicht.",
+ "optional-markdown-content": "Optional: Markdown-Inhalt hinzufügen, der direkt in der Ressource eingebettet wird",
"owners-coverage-widget-description": "Eigentümerabdeckung für alle Datenvermögenswerte im Dienst. <0>Mehr erfahren.0>",
"page-is-not-available": "Die von dir gesuchte Seite ist nicht verfügbar",
"page-sub-header-for-activity-feed": "Aktivitäts-Feed, der es dir ermöglicht, eine Zusammenfassung der Ereignisse zur Datenänderung anzuzeigen.",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "Verwerfen",
"unsaved-changes-save": "Änderungen speichern",
"unsaved-changes-warning": "Sie haben möglicherweise ungespeicherte Änderungen, die beim Schließen verworfen werden.",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "Anfrage zur Aktualisierung der Beschreibung für",
"update-displayName-entity": "Aktualisieren Sie die Anzeigenamen für das {{entity}}.",
"update-profiler-settings": "Profiler Einstellungen updaten.",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "Willkommen bei OpenMetadata!",
"welcome-to-open-metadata": "Willkommen bei {{brandName}}!",
"would-like-to-start-adding-some": "Möchten Sie mit dem Hinzufügen beginnen?",
+ "write-markdown-content": "Markdown-Inhalt hier schreiben...",
"write-your-announcement-lowercase": "Schreiben Sie Ihre Ankündigung in Kleinbuchstaben",
"write-your-description": "Schreiben Sie Ihre Beschreibung",
"write-your-text": "Schreiben Sie Ihren {{text}}",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "E-Mail erfolgreich verifiziert",
"add-entity-error": "Fehler beim Hinzufügen von {{entity}}!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "Authentifizierungsanbieter {{provider}} wird nicht für die Erneuerung von Tokens unterstützt.",
"can-not-renew-token-authentication-not-present": "Das ID-Token kann nicht erneuert werden. Der Authentifizierungsanbieter ist nicht vorhanden.",
"column-fetch-error": "Fehler beim Abrufen des Spaltentestfalls!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "Sie haben einen ungültigen Benutzernamen oder ein ungültiges Passwort eingegeben.",
"join-team-error": "Fehler beim Beitritt zum Team!",
"join-team-success": "Erfolgreich dem Team beigetreten!",
+ "learning-resources-fetch-error": "Fehler beim Abrufen der Lernressourcen",
"leave-team-error": "Fehler beim Verlassen des Teams!",
"leave-team-success": "Team erfolgreich verlassen!",
"no-application-schema-found": "Kein Anwendungsschema gefunden für {{appName}}",
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 438dde3a9757..4bd853614c7f 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
@@ -59,6 +59,7 @@
"add-new-field": "Add New Field",
"add-new-widget-plural": "Add New Widgets",
"add-product": "Add Product",
+ "add-resource": "Add Resource",
"add-row": "Add Row",
"add-suggestion": "Add suggestion",
"add-term-boost": "Add Term Boost",
@@ -315,6 +316,8 @@
"container": "Container",
"container-column": "Container Column",
"container-plural": "Containers",
+ "content-name": "Content Name",
+ "context": "Context",
"contract": "Contract",
"contract-detail-plural": "Contract Details",
"contract-execution-history": "Contract Execution History",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "Edit Glossary Display Name",
"edit-glossary-name": "Edit Glossary Name",
"edit-name": "Edit Name",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "Edit Suggestion",
"edit-widget": "Edit Widget",
"edit-widget-plural": "Edit Widgets",
@@ -597,6 +601,7 @@
"embed-file-type": "Embed {{fileType}}",
"embed-image": "Embed image",
"embed-link": "Embed link",
+ "embedded-content": "Embedded Content",
"enable": "Enable",
"enable-debug-log": "Enable Debug Log",
"enable-entity": "Enable {{entity}}",
@@ -657,6 +662,7 @@
"entity-reference-plural": "Entity References",
"entity-reference-types": "Entity Reference Types",
"entity-report-data": "Entity Report Data",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}} Running",
"entity-scheduled-to-run-value": "{{entity}} scheduled to run {{value}}",
"entity-service": "{{entity}} Service",
@@ -995,8 +1001,11 @@
"latest-offset": "Latest Offset",
"layer": "Layer",
"layer-plural": "Layers",
+ "learn-how-this-feature-works": "Learn how this feature works?",
"learn-more": "Learn More",
"learn-more-and-support": "Learn more & Support",
+ "learning-resource": "Learning Resource",
+ "learning-resources": "Learning Resources",
"leave-team": "Leave Team",
"legend": "Legend",
"less": "Less",
@@ -1025,6 +1034,7 @@
"live": "Live",
"load-more": "Load More",
"loading": "Loading",
+ "loading-article": "Loading article...",
"loading-graph": "Loading Graph",
"local-config-source": "Local Config Source",
"location": "Location",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "Mime Type",
"min": "Min",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "Mind Map",
"minor": "Minor",
"minute": "Minute",
@@ -1123,6 +1135,7 @@
"month": "Month",
"monthly": "Monthly",
"more": "More",
+ "more-action": "More Action",
"more-help": "More Help",
"more-lowercase": "more",
"most-active-user": "Most Active User",
@@ -1244,6 +1257,7 @@
"open-metadata": "OpenMetadata",
"open-metadata-logo": "OpenMetadata Logo",
"open-metadata-url": "OpenMetadata URL",
+ "open-original": "Open Original",
"open-task-plural": "Open Tasks",
"operation": "Operation",
"operation-plural": "Operations",
@@ -1273,6 +1287,7 @@
"ownership": "Ownership",
"page": "Page",
"page-not-found": "Page Not Found",
+ "page-plural": "Pages",
"page-views-by-data-asset-plural": "Page Views by Data Assets",
"parameter": "Parameter",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "Select Columns to Exclude",
"select-column-plural-to-include": "Select Columns to Include",
"select-dimension": "Select Dimension",
+ "select-duration": "Select Duration",
"select-entity": "Select {{entity}}",
"select-entity-type": "Select Entity Type",
"select-field": "Select {{field}}",
"select-field-and-weight": "Select field and its weight",
+ "select-page-plural": "Select pages",
"select-schema-object": "Select Schema Object",
+ "select-status": "Select Status",
"select-tag": "Select a tag",
"select-test-type": "Select Test Type",
"select-to-search": "Search to Select",
@@ -1675,6 +1693,7 @@
"source-label": "Source:",
"source-match": "Match By Event Source",
"source-plural": "Sources",
+ "source-provider": "Source Provider",
"source-url": "Source URL",
"source-with-details": "Source: {{source}} ({{entityName}})",
"specific-data-asset-plural": "Specific Data Assets",
@@ -1916,6 +1935,7 @@
"update-image": "Update Image",
"update-request-tag-plural": "Update Request Tags",
"updated": "Updated",
+ "updated-at": "Updated at",
"updated-by": "Updated by",
"updated-lowercase": "updated",
"updated-on": "Updated on",
@@ -2260,6 +2280,7 @@
"enter-a-field": "Enter a {{field}}",
"enter-column-description": "Enter column description",
"enter-comma-separated-field": "Enter comma(,) separated {{field}}",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "Enter display name",
"enter-feature-description": "Enter feature description",
"enter-interval": "Enter interval",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "Only external destinations can be tested.",
"failed-events-description": "Count of failed events for specific alert.",
"failed-status-for-entity-deploy": "<0>{{entity}}0> has been {{entityStatus}}, but failed to deploy",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "Failed to load image",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{failed}} failed out of {{count}} checks",
"feed-asset-action-header": "{{action}} <0>data asset0>",
"feed-custom-property-header": "updated Custom Properties on",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "Notice: The Description KPI target hasn't been met yet, but there's still time – your organization has {{count}} days left. To stay on track, please enable the Data Insights Report. This will allow us to send weekly updates to all teams, fostering collaboration and focus towards achieving our organization's KPIs.",
"latency-sla-description": "<0>{{label}}0>: Query response must be under <0>{{data}}0>",
"latest-offset-description": "The latest offset of the event in the system.",
+ "learning-resources-management-description": "Explore product features and learn how they work through our resources",
"leave-the-team-team-name": "Leave the team {{teamName}}",
"length-validator-error": "At least {{length}} {{field}} required",
"lineage-ingestion-description": "Lineage ingestion can be configured and deployed after a metadata ingestion has been set up. The lineage ingestion workflow obtains the query history, parses CREATE, INSERT, MERGE... queries and prepares the lineage between the involved entities. The lineage ingestion can have only one pipeline for a database service. Define the Query Log Duration (in days) and Result Limit to start.",
@@ -2512,6 +2536,7 @@
"no-kpi": "Define key performance indicators to monitor impact and drive smarter decisions.",
"no-kpi-available-add-new-one": "No KPIs are available. Click on the Add KPI button to add one.",
"no-kpi-found": "No KPI found with name {{name}}",
+ "no-learning-resources-available": "No learning resources available for this context.",
"no-lineage-available": "No lineage connections found",
"no-match-found": "No match found",
"no-mentions": "Looks like you and your team are all clear – no mentions in any activities just yet. Keep up the great work!",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "Only Reviewers can Approve or Reject",
"only-show-columns-with-lineage": "Only show columns with Lineage",
"optional-configuration-update-description-dbt": "Optional configuration to update the description from dbt or not",
+ "optional-markdown-content": "Optional: Add markdown content to embed directly in the resource",
"owners-coverage-widget-description": "Owners coverage for all data assets in the service. <0>learn more.0>",
"page-is-not-available": "The page you are looking for is not available",
"page-sub-header-for-activity-feed": "Activity feed that enables you view a summary of data change events.",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "Discard",
"unsaved-changes-save": "Save changes",
"unsaved-changes-warning": "You may have unsaved changes which will be discarded upon close.",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "Request to update description for",
"update-displayName-entity": "Update Display Name for the {{entity}}.",
"update-profiler-settings": "Update profiler setting.",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "Welcome to OpenMetadata!",
"welcome-to-open-metadata": "Welcome to {{brandName}}!",
"would-like-to-start-adding-some": "Would like to start adding some?",
+ "write-markdown-content": "Write markdown content here...",
"write-your-announcement-lowercase": "write your announcement",
"write-your-description": "Write your description",
"write-your-text": "Write your {{text}}",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "Email verified successfully",
"add-entity-error": "Error while adding {{entity}}!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "Auth Provider {{provider}} not supported for renewing tokens.",
"can-not-renew-token-authentication-not-present": "Cannot renew id token. Authentication Provider is not present.",
"column-fetch-error": "Error while fetching column test case!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "You have entered an invalid username or password.",
"join-team-error": "Error while joining the team!",
"join-team-success": "Team joined successfully!",
+ "learning-resources-fetch-error": "Error fetching learning resources",
"leave-team-error": "Error while leaving the team!",
"leave-team-success": "Left the team successfully!",
"no-application-schema-found": "No application schema found for {{appName}}",
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 b0f485ffad0d..6c58b1312c93 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
@@ -59,6 +59,7 @@
"add-new-field": "Agregar Nuevo Campo",
"add-new-widget-plural": "Agregar nuevos widgets",
"add-product": "Agregar Producto",
+ "add-resource": "Add Resource",
"add-row": "Añadir Fila",
"add-suggestion": "Añadir sugerencia",
"add-term-boost": "Agregar Impulso de Término",
@@ -315,6 +316,8 @@
"container": "Contenedor",
"container-column": "Contenedor Columna",
"container-plural": "Contenedores",
+ "content-name": "Content Name",
+ "context": "Contexto",
"contract": "Contrato",
"contract-detail-plural": "Detalles Contrato",
"contract-execution-history": "Historial de ejecución del contrato",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "Editar nombre de visualización del glosario",
"edit-glossary-name": "Editar nombre del glosario",
"edit-name": "Editar nombre",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "Editar sugerencia",
"edit-widget": "Editar Widget",
"edit-widget-plural": "Editar Widgets",
@@ -597,6 +601,7 @@
"embed-file-type": "{{fileType}} Embebido",
"embed-image": "Insertar imagen",
"embed-link": "Insertar enlace",
+ "embedded-content": "Contenido Incrustado",
"enable": "Habilitar",
"enable-debug-log": "Activar logs con debug",
"enable-entity": "Habilitar {{entity}}",
@@ -657,6 +662,7 @@
"entity-reference-plural": "Referencias de Entidad",
"entity-reference-types": "Tipos de Referencias de Entidad",
"entity-report-data": "Datos del Informe de Entidad",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}} Ejecutándose",
"entity-scheduled-to-run-value": "{{entity}} programado para ejecutar {{value}}",
"entity-service": "Servicio de {{entity}}",
@@ -995,8 +1001,11 @@
"latest-offset": "Último desplazamiento",
"layer": "Capa",
"layer-plural": "Capas",
+ "learn-how-this-feature-works": "¿Aprenda cómo funciona esta característica?",
"learn-more": "Más información",
"learn-more-and-support": "Más información y soporte",
+ "learning-resource": "Recurso de aprendizaje",
+ "learning-resources": "Recursos de aprendizaje",
"leave-team": "Dejar el equipo",
"legend": "Legend",
"less": "Menos",
@@ -1025,7 +1034,8 @@
"live": "Vivo",
"load-more": "Cargar más",
"loading": "Cargando",
- "loading-graph": "Cargando gráfico",
+ "loading-article": "Loading article...",
+ "loading-graph": "Loading Graph",
"local-config-source": "Origen de configuración local",
"location": "Localización",
"log-lowercase-plural": "logs",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "Tipo MIME",
"min": "Mínimo",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "Mapa mental",
"minor": "Menor",
"minute": "Minuto",
@@ -1123,6 +1135,7 @@
"month": "Mes",
"monthly": "Mensual",
"more": "Más",
+ "more-action": "More Action",
"more-help": "Más Ayuda",
"more-lowercase": "más",
"most-active-user": "Usuario Más Activo",
@@ -1244,6 +1257,7 @@
"open-metadata": "OpenMetadata",
"open-metadata-logo": "Logo de OpenMetadata",
"open-metadata-url": "URL de OpenMetadata",
+ "open-original": "Open Original",
"open-task-plural": "Tareas abiertas",
"operation": "Operación",
"operation-plural": "Operaciones",
@@ -1273,6 +1287,7 @@
"ownership": "Propiedad",
"page": "Página",
"page-not-found": "Página no encontrada",
+ "page-plural": "Páginas",
"page-views-by-data-asset-plural": "Vistas de página por activos de datos",
"parameter": "Parámetro",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "Seleccionar Columnas para Excluir",
"select-column-plural-to-include": "Seleccionar Columnas para Incluir",
"select-dimension": "Seleccionar dimensión",
+ "select-duration": "Select Duration",
"select-entity": "Seleccionar {{entity}}",
"select-entity-type": "Seleccionar tipo de entidad",
"select-field": "Seleccionar {{field}}",
"select-field-and-weight": "Seleccionar campo y su peso",
+ "select-page-plural": "Seleccionar páginas",
"select-schema-object": "Seleccionar objeto de esquema",
+ "select-status": "Select Status",
"select-tag": "Seleccionar una etiqueta",
"select-test-type": "Seleccionar tipo de prueba",
"select-to-search": "Buscar para Seleccionar",
@@ -1675,6 +1693,7 @@
"source-label": "Fuente:",
"source-match": "Coincidir por Fuente de Eventos",
"source-plural": "Fuentes",
+ "source-provider": "Proveedor de Fuente",
"source-url": "Fuente URL",
"source-with-details": "Fuente: {{source}} ({{entityName}})",
"specific-data-asset-plural": "Activos de Datos Específicos",
@@ -1916,6 +1935,7 @@
"update-image": "Actualizar imagen",
"update-request-tag-plural": "Actualizar etiquetas de solicitud",
"updated": "Actualizado",
+ "updated-at": "Updated at",
"updated-by": "Actualizado por",
"updated-lowercase": "actualizado",
"updated-on": "Actualizado el",
@@ -2260,6 +2280,7 @@
"enter-a-field": "Ingrese un {{field}}",
"enter-column-description": "Ingrese la descripción de la columna",
"enter-comma-separated-field": "Ingrese {{field}} separados por comas (,)",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "Ingrese el nombre de visualización",
"enter-feature-description": "Ingrese la descripción de la función",
"enter-interval": "Ingrese el intervalo",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "Sólo se puede comprobar los destinos externos.",
"failed-events-description": "Conteo de los eventos fallidos para alertas específicas.",
"failed-status-for-entity-deploy": "<0>{{entity}}0> se {{entityStatus}}, pero no se pudo implementar",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "Error al cargar la imagen",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{failed}} fallidas de {{count}} verificaciones",
"feed-asset-action-header": "{{action}} <0>activo de datos0>",
"feed-custom-property-header": "actualizado Propiedades Personalizadas en",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "No importa. Es hora de reestructurar tus objetivos y progresar más rápido.",
"latency-sla-description": "<0>{{label}}0>: La respuesta a la consulta debe ser menor que <0>{{data}}0>.",
"latest-offset-description": "El último offset del evento en el sistema.",
+ "learning-resources-management-description": "Explora las funciones del producto y aprende cómo funcionan a través de nuestros recursos",
"leave-the-team-team-name": "Abandonar el equipo {{teamName}}",
"length-validator-error": "Se requiere al menos {{length}} {{field}}",
"lineage-ingestion-description": "La ingesta de linaje se puede configurar y implementar después de que se haya establecido una ingesta de metadatos. El workflow de ingesta de linaje obtiene el historial de consultas, analiza las consultas CREATE, INSERT, MERGE, etc. y prepara el linaje entre las entidades involucradas. La ingesta de linaje solo puede tener un workflow para un servicio de base de datos. Defina la duración del registro de consultas (en días) y el límite de resultados para comenzar.",
@@ -2512,6 +2536,7 @@
"no-kpi": "Define indicadores clave de rendimiento para monitorear el impacto y tomar decisiones más inteligentes.",
"no-kpi-available-add-new-one": "No hay KPIs disponibles. Haz clic en el botón 'Agregar KPI' para agregar uno.",
"no-kpi-found": "No se encontró un KPI con el nombre {{name}}",
+ "no-learning-resources-available": "No hay recursos de aprendizaje disponibles para este contexto.",
"no-lineage-available": "No se encontraron conexiones de lineage",
"no-match-found": "No se encontraron coincidencias",
"no-mentions": "No hay instancias en las que tú o tu equipo hayan sido mencionados en ninguna actividad",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "Solo los Revisores pueden Aprobar o Rechazar",
"only-show-columns-with-lineage": "Mostrar solo columnas con Lineage",
"optional-configuration-update-description-dbt": "Configuración opcional para actualizar la descripción de dbt",
+ "optional-markdown-content": "Opcional: Añadir contenido markdown para incrustar directamente en el recurso",
"owners-coverage-widget-description": "Cobertura de propietarios para todos los activos de datos en el servicio. <0>saber más.0>",
"page-is-not-available": "La página que estás buscando no está disponible",
"page-sub-header-for-activity-feed": "Feed de actividad que te permite ver un resumen de los eventos de cambio de datos.",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "Descartar",
"unsaved-changes-save": "Guardar cambios",
"unsaved-changes-warning": "Puede que tengas cambios no guardados que se descartarán al cerrar.",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "Solicitud para actualizar la descripción de",
"update-displayName-entity": "Actualizar el nombre visualizado para el {{entity}}.",
"update-profiler-settings": "Actualizar la configuración del perfilador.",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "¡Bienvenido a OpenMetadata!",
"welcome-to-open-metadata": "¡Bienvenido a {{brandName}}!",
"would-like-to-start-adding-some": "¿Te gustaría comenzar a agregar algunos?",
+ "write-markdown-content": "Escribe contenido markdown aquí...",
"write-your-announcement-lowercase": "escribe tu anuncio",
"write-your-description": "Escribe tu descripción",
"write-your-text": "Escribe tu {{text}}",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "Verificación de correo electrónico realizada con éxito",
"add-entity-error": "¡Error al agregar {{entity}}!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "Proveedor de autenticación {{provider}} no soportado para renovar tokens.",
"can-not-renew-token-authentication-not-present": "No se puede renovar el token de identificación. El proveedor de autenticación no está presente.",
"column-fetch-error": "¡Error al obtener la columna de la prueba!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "Has ingresado un nombre de usuario o contraseña inválido.",
"join-team-error": "¡Error al unirse al equipo!",
"join-team-success": "¡Equipo unido exitosamente!",
+ "learning-resources-fetch-error": "Error al obtener los recursos de aprendizaje",
"leave-team-error": "¡Error al abandonar el equipo!",
"leave-team-success": "¡Abandonaste el equipo exitosamente!",
"no-application-schema-found": "No se encontró esquema de aplicación para {{appName}}",
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 75180f95cd2b..5af98bb3222d 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
@@ -59,6 +59,7 @@
"add-new-field": "Ajouter un nouveau champ",
"add-new-widget-plural": "Ajouter de nouveaux widgets",
"add-product": "Ajouter un Produit",
+ "add-resource": "Add Resource",
"add-row": "Ajouter une Ligne",
"add-suggestion": "Ajouter une suggestion",
"add-term-boost": "Ajouter un Boost de Terme",
@@ -315,6 +316,8 @@
"container": "Conteneur",
"container-column": "Colonne de Conteneur",
"container-plural": "Conteneurs",
+ "content-name": "Content Name",
+ "context": "Contexte",
"contract": "Contrat",
"contract-detail-plural": "Détails du Contrat",
"contract-execution-history": "Historique d'exécution du contrat",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "Éditer le Nom d'Affichage du Glossaire",
"edit-glossary-name": "Éditer le Nom du Glossaire",
"edit-name": "Modifier le nom",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "Modifier la suggestion",
"edit-widget": "Modifier le Widget",
"edit-widget-plural": "Éditer les Widgets",
@@ -597,6 +601,7 @@
"embed-file-type": "Embed {{fileType}}",
"embed-image": "Incorporer image",
"embed-link": "Incorporer lien",
+ "embedded-content": "Contenu intégré",
"enable": "Activer",
"enable-debug-log": "Activer le Journal de Débogage",
"enable-entity": "Activer {{entity}}",
@@ -657,6 +662,7 @@
"entity-reference-plural": "Références d'Entité",
"entity-reference-types": "Types de Référence d'Entité",
"entity-report-data": "Données de Rapport d'Entité",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}} en cours d'exécution",
"entity-scheduled-to-run-value": "{{entity}} programmé pour s'exécuter {{value}}",
"entity-service": "Service de {{entity}}",
@@ -995,8 +1001,11 @@
"latest-offset": "Dernier décalage",
"layer": "Couche",
"layer-plural": "Couches",
+ "learn-how-this-feature-works": "Découvrez comment cette fonctionnalité fonctionne ?",
"learn-more": "En savoir plus",
"learn-more-and-support": "En savoir plus et Support",
+ "learning-resource": "Ressource d'apprentissage",
+ "learning-resources": "Ressources d'apprentissage",
"leave-team": "Quitter une Équipe",
"legend": "Légende",
"less": "Moins",
@@ -1025,7 +1034,8 @@
"live": "En Direct",
"load-more": "Plus",
"loading": "Chargement",
- "loading-graph": "Chargement du Graphique",
+ "loading-article": "Loading article...",
+ "loading-graph": "Loading Graph",
"local-config-source": "Source de Configuration Locale",
"location": "Emplacement",
"log-lowercase-plural": "logs",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "Type MIME",
"min": "Min",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "Carte mentale",
"minor": "Mineur",
"minute": "Minute",
@@ -1123,6 +1135,7 @@
"month": "Mois",
"monthly": "Mensuel",
"more": "Plus",
+ "more-action": "More Action",
"more-help": "Plus d'Aide",
"more-lowercase": "plus",
"most-active-user": "Utilisateurs les plus Actifs",
@@ -1244,6 +1257,7 @@
"open-metadata": "OpenMetadata",
"open-metadata-logo": "Logo OpenMetadata",
"open-metadata-url": "URL OpenMetadata",
+ "open-original": "Open Original",
"open-task-plural": "Tâches Ouverts",
"operation": "Opération",
"operation-plural": "Opérations",
@@ -1273,6 +1287,7 @@
"ownership": "Propriété",
"page": "Page",
"page-not-found": "Page Non Trouvée",
+ "page-plural": "Pages",
"page-views-by-data-asset-plural": "Vues de Page par Actif de Données",
"parameter": "Paramètre",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "Sélectionner les Colonnes à Exclure",
"select-column-plural-to-include": "Sélectionner les Colonnes à Inclure",
"select-dimension": "Sélectionner une dimension",
+ "select-duration": "Select Duration",
"select-entity": "Sélectionner {{entity}}",
"select-entity-type": "Sélectionner le type d'entité",
"select-field": "Sélectionner le Champ {{field}}",
"select-field-and-weight": "Sélectionner le Champ et son Poids",
+ "select-page-plural": "Sélectionner des pages",
"select-schema-object": "Sélectionner un objet de schéma",
+ "select-status": "Select Status",
"select-tag": "Sélectionner un tag",
"select-test-type": "Sélectionner le type de test",
"select-to-search": "Sélectionner pour Rechercher",
@@ -1675,6 +1693,7 @@
"source-label": "Source:",
"source-match": "Correspondance par Source d'Événement",
"source-plural": "Sources",
+ "source-provider": "Fournisseur source",
"source-url": "URL Source",
"source-with-details": "Source: {{source}} ({{entityName}})",
"specific-data-asset-plural": "Actifs de Données Spécifiques",
@@ -1916,6 +1935,7 @@
"update-image": "Mettre à Jour l'Image",
"update-request-tag-plural": "Mettre à Jour la Demande de Tags",
"updated": "Mis à Jour",
+ "updated-at": "Updated at",
"updated-by": "Mis à Jour par",
"updated-lowercase": "mis à jour",
"updated-on": "Mis à Jour le",
@@ -2260,6 +2280,7 @@
"enter-a-field": "Entrer un {{field}}",
"enter-column-description": "Entrer la Description de la Colonne",
"enter-comma-separated-field": "Entrer {{field}} séparé par une virgule(,)",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "Entrer un nom d'affichage",
"enter-feature-description": "Entrer une description pour la caractéristique",
"enter-interval": "Entrer un interval",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "Seules les destinations externes peuvent être testées.",
"failed-events-description": "Nombre d'événements échoués pour une alerte spécifique.",
"failed-status-for-entity-deploy": "<0>{{entity}}0> a été {{entityStatus}}, mais n'a pas pu être déployé",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "Échec du chargement de l'image",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{failed}} échecs sur {{count}} vérifications",
"feed-asset-action-header": "{{action}} <0>actif de données0>",
"feed-custom-property-header": "a mis à jour la propriété personnalisée le",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "Dommage. Vous devriez peut-être réévaluer vos objectifs pour continuer votre progression.",
"latency-sla-description": "<0>{{label}}0> : le temps de réponse de la requête doit être inférieur à <0>{{data}}0>.",
"latest-offset-description": "Le dernier décalage de l'événement dans le système.",
+ "learning-resources-management-description": "Explorez les fonctionnalités du produit et découvrez comment elles fonctionnent grâce à nos ressources",
"leave-the-team-team-name": "Quitter l'équipe {{teamName}}",
"length-validator-error": "Au moins {{length}} {{field}} requis",
"lineage-ingestion-description": "Traçabilité de l'ingestion peut être configurée et déployée après une ingestion de métadonnées a été mis en place. Le flux de travail d'ingestion de la lignée obtient l'historique des requêtes, analyse CREATE, INSERT, MERGE... requêtes et prépare la lignée entre les entités impliquées. L'ingestion de la lignée peut avoir un seul pipeline pour un service de base de données. Définir la durée du journal de requêtes (en jours) et le nombre de résultats pour démarrer.",
@@ -2512,6 +2536,7 @@
"no-kpi": "Définissez des indicateurs clés de performance pour surveiller l'impact et prendre des décisions plus éclairées.",
"no-kpi-available-add-new-one": "Aucun KPI n'est disponible, ajoutez un KPI en cliquant sur le bouton \"Ajouter un KPI\".",
"no-kpi-found": "Aucun KPI trouvé avec le nom {{name}}",
+ "no-learning-resources-available": "Aucune ressource d'apprentissage disponible pour ce contexte.",
"no-lineage-available": "Aucune connexion de lineage trouvée",
"no-match-found": "Aucune correspondance trouvée",
"no-mentions": "Il n'y a aucune instance où vous ou votre équipe avez été mentionnés dans des activités",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "Seuls les réviseurs peuvent Valider ou Rejeter",
"only-show-columns-with-lineage": "Afficher uniquement les colonnes avec Lineage",
"optional-configuration-update-description-dbt": "Configuration optionnelle pour mettre à jour la description à partir de dbt ou non",
+ "optional-markdown-content": "Facultatif : Ajouter du contenu markdown à intégrer directement dans la ressource",
"owners-coverage-widget-description": "Couverture des propriétaires pour tous les actifs de données dans le service. <0>en savoir plus.0>",
"page-is-not-available": "La page que vous recherchez n'est pas disponible",
"page-sub-header-for-activity-feed": "Fil d'activité qui vous permet de voir un résumé des événements de modification des données.",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "Abandonner",
"unsaved-changes-save": "Sauvegarder les modifications",
"unsaved-changes-warning": "Vous pourriez avoir des modifications non enregistrées qui seront perdues à la fermeture.",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "demander la mise à jour de la description pour",
"update-displayName-entity": "Mettre à Jour le Nom d'Affichage de {{entity}}.",
"update-profiler-settings": "Mettre à jour les réglages du profiler.",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "Bienvenue dans OpenMetadata !",
"welcome-to-open-metadata": "Bienvenue dans {{brandName}} !",
"would-like-to-start-adding-some": "Souhaitez-vous commencer à ajouter des",
+ "write-markdown-content": "Écrire du contenu markdown ici...",
"write-your-announcement-lowercase": "écrire votre annonce",
"write-your-description": "Rédigez votre description",
"write-your-text": "Ecrivez votre {{text}}",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "Email vérifié avec succès",
"add-entity-error": "Erreur lors de l'ajout de {{entity}} !",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "Le fournisseur d'authentification {{provider}} n'est pas pris en charge pour le renouvellement des jetons.",
"can-not-renew-token-authentication-not-present": "Impossible de renouveler le jeton d'identification. L'authentification n'est pas présente.",
"column-fetch-error": "Erreur lors de la récupération de la colonne de cas de test !",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "Vous avez saisi un nom d'utilisateur ou un mot de passe invalide.",
"join-team-error": "Une erreur est survenue lorsque vous avez rejoint l'équipe !",
"join-team-success": "Vous avez rejoint l'équipe avec succès !",
+ "learning-resources-fetch-error": "Erreur lors de la récupération des ressources d'apprentissage",
"leave-team-error": "Une erreur est survenue lorsque vous avez quitté l'équipe !",
"leave-team-success": "Vous avez quitté l'équipe avec succès !",
"no-application-schema-found": "Aucun schéma d'application trouvé pour {{appName}}",
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 343b8d3b3f3b..f3911dad9d35 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
@@ -59,6 +59,7 @@
"add-new-field": "Engadir Novo Campo",
"add-new-widget-plural": "Engadir novos widgets",
"add-product": "Engadir Produto",
+ "add-resource": "Add Resource",
"add-row": "Engadir fila",
"add-suggestion": "Engadir suxestión",
"add-term-boost": "Engadir Impulso de Termo",
@@ -315,6 +316,8 @@
"container": "Contedor",
"container-column": "Columna do contedor",
"container-plural": "Contedores",
+ "content-name": "Content Name",
+ "context": "Contexto",
"contract": "Contrato",
"contract-detail-plural": "Detalles do Contrato",
"contract-execution-history": "Historial de ejecución do contrato",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "Editar nome de visualización do glosario",
"edit-glossary-name": "Editar nome do glosario",
"edit-name": "Editar nome",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "Editar suxestión",
"edit-widget": "Editar Widget",
"edit-widget-plural": "Editar widgets",
@@ -597,6 +601,7 @@
"embed-file-type": "Embed {{fileType}}",
"embed-image": "Incrustar imaxe",
"embed-link": "Incrustar ligazón",
+ "embedded-content": "Contido Incorporado",
"enable": "Activar",
"enable-debug-log": "Activar rexistro de depuración",
"enable-entity": "Habilitar {{entity}}",
@@ -657,6 +662,7 @@
"entity-reference-plural": "Referencias de {{entity}}",
"entity-reference-types": "Tipos de referencia de {{entity}}",
"entity-report-data": "Datos do Informe de Entidade",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}} Executándose",
"entity-scheduled-to-run-value": "{{entity}} programado para executar {{value}}",
"entity-service": "Servizo de {{entity}}",
@@ -995,8 +1001,11 @@
"latest-offset": "Último desprazamento",
"layer": "Capa",
"layer-plural": "Capas",
+ "learn-how-this-feature-works": "Aprenda como funciona esta función?",
"learn-more": "Aprender máis",
"learn-more-and-support": "Aprender máis e soporte",
+ "learning-resource": "Recurso de aprendizaxe",
+ "learning-resources": "Recursos de aprendizaxe",
"leave-team": "Abandonar equipo",
"legend": "Lenda",
"less": "Menos",
@@ -1025,6 +1034,7 @@
"live": "En directo",
"load-more": "Cargar máis",
"loading": "Cargando",
+ "loading-article": "Loading article...",
"loading-graph": "Cargando gráfico",
"local-config-source": "Fonte de configuración local",
"location": "Localización",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "Tipo MIME",
"min": "Mínimo",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "Mapa mental",
"minor": "Menor",
"minute": "Minuto",
@@ -1123,6 +1135,7 @@
"month": "Mes",
"monthly": "Mensual",
"more": "Máis",
+ "more-action": "More Action",
"more-help": "Máis axuda",
"more-lowercase": "máis",
"most-active-user": "Usuario máis activo",
@@ -1244,6 +1257,7 @@
"open-metadata": "OpenMetadata",
"open-metadata-logo": "Logotipo de OpenMetadata",
"open-metadata-url": "URL de OpenMetadata",
+ "open-original": "Open Original",
"open-task-plural": "Tarefas abertas",
"operation": "Operación",
"operation-plural": "Operacións",
@@ -1273,6 +1287,7 @@
"ownership": "Propiedad",
"page": "Páxina",
"page-not-found": "Páxina non atopada",
+ "page-plural": "Páxinas",
"page-views-by-data-asset-plural": "Visualizacións de páxinas por activos de datos",
"parameter": "Parámetro",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "Seleccionar columnas para excluír",
"select-column-plural-to-include": "Seleccionar columnas para incluír",
"select-dimension": "Seleccionar dimensión",
+ "select-duration": "Select Duration",
"select-entity": "Seleccionar {{entity}}",
"select-entity-type": "Seleccionar o tipo de entidade",
"select-field": "Seleccionar {{field}}",
"select-field-and-weight": "Seleccionar campo e o seu peso",
+ "select-page-plural": "Seleccionar páxinas",
"select-schema-object": "Seleccionar obxecto de esquema",
+ "select-status": "Select Status",
"select-tag": "Seleccionar unha etiqueta",
"select-test-type": "Seleccionar tipo de proba",
"select-to-search": "Buscar para seleccionar",
@@ -1675,6 +1693,7 @@
"source-label": "Source:",
"source-match": "Coincidencia por fonte de evento",
"source-plural": "Fontes",
+ "source-provider": "Provedor de Orixe",
"source-url": "URL da fonte",
"source-with-details": "Source: {{source}} ({{entityName}})",
"specific-data-asset-plural": "Activos de datos específicos",
@@ -1916,6 +1935,7 @@
"update-image": "Actualizar imaxe",
"update-request-tag-plural": "Actualizar etiquetas de solicitude",
"updated": "Actualizado",
+ "updated-at": "Updated at",
"updated-by": "Actualizado por",
"updated-lowercase": "actualizado",
"updated-on": "Actualizado o",
@@ -2260,6 +2280,7 @@
"enter-a-field": "Introduce un {{field}}",
"enter-column-description": "Introduce a descrición da columna",
"enter-comma-separated-field": "Introduce {{field}} separado por comas (,)",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "Introduce o nome de visualización",
"enter-feature-description": "Introduce a descrición da característica",
"enter-interval": "Introduce o intervalo",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "Só se poden probar destinos externos.",
"failed-events-description": "Count of failed events for specific alert.",
"failed-status-for-entity-deploy": "<0>{{entity}}0> foi {{entityStatus}}, pero fallou o despregamento",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "Erro ao cargar a imaxe",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{failed}} fallidas de {{count}} verificacións",
"feed-asset-action-header": "{{action}} <0>activo de datos0>",
"feed-custom-property-header": "actualizou Propiedades Personalizadas en",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "Aviso: O obxectivo KPI de Descrición aínda non se alcanzou, pero aínda hai tempo: á túa organización quédanlle {{count}} días. Para manter o rumbo, habilita o Informe de Insights de Datos. Isto permitirá enviar actualizacións semanais a todos os equipos, fomentando a colaboración e o enfoque cara a alcanzar os KPI da nosa organización.",
"latency-sla-description": "<0>{{label}}0>: A resposta á consulta debe ser inferior a <0>{{data}}0>.",
"latest-offset-description": "The latest offset of the event in the system.",
+ "learning-resources-management-description": "Explora as funcións do produto e aprende como funcionan a través dos nosos recursos",
"leave-the-team-team-name": "Abandonar o equipo {{teamName}}",
"length-validator-error": "Requírese polo menos {{length}} {{field}}",
"lineage-ingestion-description": "A inxestión de liñaxe pódese configurar e despregar despois de configurar a inxestión de metadatos. O fluxo de traballo de inxestión de liñaxe obtén o historial de consultas, analiza consultas CREATE, INSERT, MERGE... e prepara a liñaxe entre as entidades implicadas. A inxestión de liñaxe só pode ter un pipeline para un servizo de base de datos. Define a Duración do Rexistro de Consultas (en días) e o Límite de Resultados para comezar.",
@@ -2512,6 +2536,7 @@
"no-kpi": "Define indicadores clave de rendemento para monitorizar o impacto e tomar decisións máis intelixentes.",
"no-kpi-available-add-new-one": "Non hai KPI dispoñibles. Fai clic no botón Engadir KPI para engadir un.",
"no-kpi-found": "Non se atopou ningún KPI co nome {{name}}",
+ "no-learning-resources-available": "Non hai recursos de aprendizaxe dispoñibles para este contexto.",
"no-lineage-available": "Non se atoparon conexións de lineage",
"no-match-found": "Non se atopou ningunha coincidencia",
"no-mentions": "Parece que ti e o teu equipo estades ao día: non hai mencións en ningunha actividade por agora. Seguídeo así!",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "Só os revisores poden aprobar ou rexeitar",
"only-show-columns-with-lineage": "Mostrar só columnas con Lineage",
"optional-configuration-update-description-dbt": "Configuración opcional para actualizar ou non a descrición desde dbt",
+ "optional-markdown-content": "Opcional: Engadir contido markdown para incorporar directamente no recurso",
"owners-coverage-widget-description": "Cobertura de propietarios para todos os activos de datos no servizo. <0>saber máis.0>",
"page-is-not-available": "A páxina que estás buscando non está dispoñible",
"page-sub-header-for-activity-feed": "Feed de actividades que che permite ver un resumo dos eventos de cambio de datos.",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "Descartar",
"unsaved-changes-save": "Gardar cambios",
"unsaved-changes-warning": "Podes ter cambios non gardados que se descartarán ao pechar.",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "Solicitude de actualización de descrición para",
"update-displayName-entity": "Actualizar o Nome de Visualización para {{entity}}.",
"update-profiler-settings": "Actualizar a configuración do perfilador.",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "Benvido a OpenMetadata!",
"welcome-to-open-metadata": "Benvido a {{brandName}}!",
"would-like-to-start-adding-some": "Gustaríache comezar a engadir algúns?",
+ "write-markdown-content": "Escribe contido markdown aquí...",
"write-your-announcement-lowercase": "escribe o teu anuncio",
"write-your-description": "Escribe a túa descrición",
"write-your-text": "Escribe o teu {{text}}",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "Correo electrónico verificado correctamente",
"add-entity-error": "Erro ao engadir {{entity}}!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "O provedor de autenticación {{provider}} non é compatible para renovar tokens.",
"can-not-renew-token-authentication-not-present": "Non se pode renovar o token de identificación. O provedor de autenticación non está presente.",
"column-fetch-error": "Erro ao obter o caso de proba de columna!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "Introduciches un nome de usuario ou contrasinal non válido.",
"join-team-error": "Erro ao unirse ao equipo!",
"join-team-success": "Unido ao equipo correctamente!",
+ "learning-resources-fetch-error": "Erro ao obter os recursos de aprendizaxe",
"leave-team-error": "Erro ao abandonar o equipo!",
"leave-team-success": "Abandonado o equipo correctamente!",
"no-application-schema-found": "Non se atopou esquema de aplicación para {{appName}}",
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 0e940ff68f53..247864cb2e64 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
@@ -59,6 +59,7 @@
"add-new-field": "הוסף שדה חדש",
"add-new-widget-plural": "הוסף ווידג'טים חדשים",
"add-product": "הוסף מוצר",
+ "add-resource": "Add Resource",
"add-row": "הוסף שורה",
"add-suggestion": "הוסף הצעה",
"add-term-boost": "הוסף הגברת מונח",
@@ -315,6 +316,8 @@
"container": "אחסון",
"container-column": "Container Column",
"container-plural": "אחסון (Storage)",
+ "content-name": "Content Name",
+ "context": "הקשר",
"contract": "חוזה",
"contract-detail-plural": "פרטי חוזה",
"contract-execution-history": "סטטוס ביצוע חוזה",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "ערוך שם תצוגת מילון מונחים",
"edit-glossary-name": "ערוך שם מילון מונחים",
"edit-name": "ערוך שם",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "ערוך הצעה",
"edit-widget": "ערוך ווידג'ט",
"edit-widget-plural": "ערוך ווידג'טים",
@@ -597,6 +601,7 @@
"embed-file-type": "Embed {{fileType}}",
"embed-image": "הטמע תמונה",
"embed-link": "הטמע קישור",
+ "embedded-content": "תוכן מוטמע",
"enable": "הפעל",
"enable-debug-log": "הפעל יומן דיבוג",
"enable-entity": "הפעל {{entity}}",
@@ -657,6 +662,7 @@
"entity-reference-plural": "Entity References",
"entity-reference-types": "Entity Reference Types",
"entity-report-data": "נתוני דוח ישות",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}} פועל",
"entity-scheduled-to-run-value": "{{entity}} מתוכנן לרוץ {{value}}",
"entity-service": "שירות {{entity}}",
@@ -995,8 +1001,11 @@
"latest-offset": "היסט אחרון",
"layer": "Layer",
"layer-plural": "Layers",
+ "learn-how-this-feature-works": "למד כיצד תכונה זו עובדת?",
"learn-more": "Learn More",
"learn-more-and-support": "למידע נוסף ותמיכה",
+ "learning-resource": "משאב למידה",
+ "learning-resources": "משאבי למידה",
"leave-team": "צא מהצוות",
"legend": "Legend",
"less": "פחות",
@@ -1025,7 +1034,8 @@
"live": "שידור חי",
"load-more": "טען עוד",
"loading": "טוען",
- "loading-graph": "טוען גרף",
+ "loading-article": "Loading article...",
+ "loading-graph": "Loading Graph",
"local-config-source": "מקור תצורה מקומי",
"location": "מיקום",
"log-lowercase-plural": "logs",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "סוג MIME",
"min": "מינימום",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "מפת חשיבה",
"minor": "משני",
"minute": "דקה",
@@ -1123,6 +1135,7 @@
"month": "חודש",
"monthly": "חודשי",
"more": "יותר",
+ "more-action": "More Action",
"more-help": "עזרה נוספת",
"more-lowercase": "יותר",
"most-active-user": "המשתמש הפעיל ביותר",
@@ -1244,6 +1257,7 @@
"open-metadata": "OpenMetadata",
"open-metadata-logo": "OpenMetadata Logo",
"open-metadata-url": "OpenMetadata URL",
+ "open-original": "Open Original",
"open-task-plural": "Open Tasks",
"operation": "פעולה",
"operation-plural": "פעולות",
@@ -1273,6 +1287,7 @@
"ownership": "בעלות",
"page": "עמוד",
"page-not-found": "הדף לא נמצא",
+ "page-plural": "דפים",
"page-views-by-data-asset-plural": "צפיות בדפים לפי נכסי נתונים",
"parameter": "פרמטר",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "בחר עמודות לא לכלול",
"select-column-plural-to-include": "בחר עמודות לכלול",
"select-dimension": "בחר מימד",
+ "select-duration": "Select Duration",
"select-entity": "Select {{entity}}",
"select-entity-type": "בחירת סוג האובייקט",
"select-field": "בחר {{field}}",
"select-field-and-weight": "בחר שדה ומשקלו",
+ "select-page-plural": "בחר דפים",
"select-schema-object": "בחר אובייקט סכימה",
+ "select-status": "Select Status",
"select-tag": "בחר תג",
"select-test-type": "בחר סוג בדיקה",
"select-to-search": "חפש כדי לבחור",
@@ -1675,6 +1693,7 @@
"source-label": "Source:",
"source-match": "התאמה לפי מקור אירוע",
"source-plural": "מקורות",
+ "source-provider": "ספק מקור",
"source-url": "כתובת מקור",
"source-with-details": "מקור: {{source}} ({{entityName}})",
"specific-data-asset-plural": "נכסי נתונים ספציפיים",
@@ -1916,6 +1935,7 @@
"update-image": "עדכן תמונה",
"update-request-tag-plural": "עדכן תגי בקשה",
"updated": "מעודכן",
+ "updated-at": "Updated at",
"updated-by": "עודכן על ידי",
"updated-lowercase": "מעודכן",
"updated-on": "עודכן בתאריך",
@@ -2260,6 +2280,7 @@
"enter-a-field": "הזן {{field}}",
"enter-column-description": "הזן תיאור עמודה",
"enter-comma-separated-field": "הזן {{field}} מופרדות בפסיק (,)",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "הזן שם תצוגה",
"enter-feature-description": "הזן תיאור מאפיין",
"enter-interval": "הזן קצב",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "רק יעדים חיצוניים ניתנים לבדיקה.",
"failed-events-description": "ספירת אירועים שנכשלו עבור התראה ספציפית.",
"failed-status-for-entity-deploy": "<0>{{entity}}0> נוצרה בהצלחה, אך נכשלה בפרסום",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "טעינת התמונה נכשלה",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{failed}} נכשלו מתוך {{count}} בדיקות",
"feed-asset-action-header": "{{action}} <0>data asset0>",
"feed-custom-property-header": "updated Custom Properties on",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "לא משנה. זמן לשדרג את המטרות שלך ולהתקדם במהירות יותר.",
"latency-sla-description": "<0>{{label}}0>: זמן תגובת השאילתה חייב להיות פחות מ‑ <0>{{data}}0>.",
"latest-offset-description": "ה-offset האחרון של האירוע במערכת.",
+ "learning-resources-management-description": "חקור את תכונות המוצר ולמד כיצד הן עובדות באמצעות המשאבים שלנו",
"leave-the-team-team-name": "עזוב את הקבוצה {{teamName}}",
"length-validator-error": "נדרשים לפחות {{length}} {{field}}",
"lineage-ingestion-description": "הקליטת השקפיות יכולה להיות מוגדרת ופורסמת לאחר שהקליטת מטא-דאטה הוגדרה. תהליך העבודה של הקליטת שקפיות משיג את ההיסטוריה של השאילתות, פורס את השאילתות CREATE, INSERT, MERGE... ומכין את השקפיות בין היישויות המעורבות. הקליטת שקפיות יכולה להכיל רק תהליך שינוע נתונים אחד עבור שירות בסיס נתונים. הגדר את משך יומיות יומן השאילתות (בימים) והגבלת התוצאה להתחלה.",
@@ -2512,6 +2536,7 @@
"no-kpi": "הגדר מדדי ביצוע מרכזיים כדי לעקוב אחר השפעה ולקבל החלטות חכמות יותר.",
"no-kpi-available-add-new-one": "אין KPIים זמינים. לחץ על כפתור הוספת KPI כדי להוסיף אחד.",
"no-kpi-found": "לא נמצא KPI עם השם {{name}}",
+ "no-learning-resources-available": "אין משאבי למידה זמינים להקשר זה.",
"no-lineage-available": "לא נמצאו קישורי lineage",
"no-match-found": "לא נמצאה התאמה",
"no-mentions": "אין מקרים בהם התייחסו אליך או לצוות שלך בפעולות כלשהן",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "רק בודקים יכולים לאשר או לדחות",
"only-show-columns-with-lineage": "הצג רק עמודות עם Lineage",
"optional-configuration-update-description-dbt": "הגדרת אופציונלית לעדכון התיאור מ-dbT או לא",
+ "optional-markdown-content": "אופציונלי: הוסף תוכן markdown להטמעה ישירות במשאב",
"owners-coverage-widget-description": "כיסוי בעלים עבור כל נכסי הנתונים בשירות. <0>למד עוד0>.",
"page-is-not-available": "הדף שאתה מחפש לא זמין",
"page-sub-header-for-activity-feed": "הזרמת פעילות שמאפשרת לך לראות סיכומים של אירועי שינוי בנתונים.",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "מחק",
"unsaved-changes-save": "שמור שינויים",
"unsaved-changes-warning": "ייתכן שיש לך שינויים שלא נשמרו שיימחקו בעת הסגירה.",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "Request to update description for",
"update-displayName-entity": "עדכן את השם המוצג עבור {{entity}}.",
"update-profiler-settings": "עדכן הגדרות הפרופיילר.",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "ברוך הבא ל-OpenMetadata!",
"welcome-to-open-metadata": "ברוך הבא ל-{{brandName}}!",
"would-like-to-start-adding-some": "רוצה להתחיל להוסיף משהו?",
+ "write-markdown-content": "כתוב תוכן markdown כאן...",
"write-your-announcement-lowercase": "כתוב את ההודעה שלך",
"write-your-description": "כתוב את התיאור שלך",
"write-your-text": "כתוב את ה-{{text}} שלך",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "אימייל אומת בהצלחה",
"add-entity-error": "שגיאה במהלך הוספת {{entity}}!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "ספק האימות {{provider}} אינו נתמך לחידוש טוקנים.",
"can-not-renew-token-authentication-not-present": "אין אפשרות לחדש טוקן ID. ספק האימות אינו נמצא.",
"column-fetch-error": "שגיאה במהלך אחזור מבחן העמודה!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "הוזן שם משתמש או סיסמה לא תקינים.",
"join-team-error": "שגיאה במהלך הצטרפות לקבוצה!",
"join-team-success": "הצטרפות לקבוצה בהצלחה!",
+ "learning-resources-fetch-error": "שגיאה בטעינת משאבי למידה",
"leave-team-error": "שגיאה במהלך יציאה מהקבוצה!",
"leave-team-success": "יציאה מהקבוצה בהצלחה!",
"no-application-schema-found": "לא נמצאה סכמת יישום עבור {{appName}}",
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 e93912320bdf..07d22f35c896 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
@@ -59,6 +59,7 @@
"add-new-field": "新しいフィールドを追加",
"add-new-widget-plural": "新しいウィジェットを追加",
"add-product": "製品を追加",
+ "add-resource": "Add Resource",
"add-row": "行を追加",
"add-suggestion": "提案を追加",
"add-term-boost": "用語ブーストを追加",
@@ -315,6 +316,8 @@
"container": "コンテナ",
"container-column": "コンテナカラム",
"container-plural": "コンテナ",
+ "content-name": "Content Name",
+ "context": "コンテキスト",
"contract": "契約",
"contract-detail-plural": "契約詳細",
"contract-execution-history": "契約実行履歴",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "用語集の表示名を編集",
"edit-glossary-name": "用語集の名前を編集",
"edit-name": "名前を編集",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "提案を編集",
"edit-widget": "ウィジェットを編集",
"edit-widget-plural": "ウィジェットを編集",
@@ -597,6 +601,7 @@
"embed-file-type": "{{fileType}} を埋め込む",
"embed-image": "画像を埋め込む",
"embed-link": "リンクを埋め込む",
+ "embedded-content": "埋め込みコンテンツ",
"enable": "有効化",
"enable-debug-log": "デバッグログを有効化",
"enable-entity": "{{entity}} を有効化",
@@ -657,6 +662,7 @@
"entity-reference-plural": "エンティティ参照",
"entity-reference-types": "エンティティ参照タイプ",
"entity-report-data": "エンティティレポートデータ",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}}実行中",
"entity-scheduled-to-run-value": "{{entity}}が{{value}}に実行予定",
"entity-service": "{{entity}}サービス",
@@ -995,8 +1001,11 @@
"latest-offset": "最新のオフセット",
"layer": "レイヤー",
"layer-plural": "レイヤー",
+ "learn-how-this-feature-works": "この機能の仕組みを学びますか?",
"learn-more": "詳細を見る",
"learn-more-and-support": "詳細とサポート",
+ "learning-resource": "学習リソース",
+ "learning-resources": "学習リソース",
"leave-team": "チームを離脱",
"legend": "Legend",
"less": "少ない",
@@ -1025,7 +1034,8 @@
"live": "ライブ",
"load-more": "さらに読み込む",
"loading": "読み込み中",
- "loading-graph": "グラフ読み込み中",
+ "loading-article": "Loading article...",
+ "loading-graph": "Loading Graph",
"local-config-source": "ローカル設定ソース",
"location": "場所",
"log-lowercase-plural": "logs",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "MIMEタイプ",
"min": "最小",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "マインドマップ",
"minor": "マイナー",
"minute": "分",
@@ -1123,6 +1135,7 @@
"month": "月",
"monthly": "毎月",
"more": "もっと見る",
+ "more-action": "More Action",
"more-help": "さらに詳しいヘルプ",
"more-lowercase": "もっと",
"most-active-user": "最もアクティブなユーザー",
@@ -1244,6 +1257,7 @@
"open-metadata": "OpenMetadata",
"open-metadata-logo": "OpenMetadata ロゴ",
"open-metadata-url": "OpenMetadata URL",
+ "open-original": "Open Original",
"open-task-plural": "未完了のタスク",
"operation": "操作",
"operation-plural": "操作",
@@ -1273,6 +1287,7 @@
"ownership": "所有権",
"page": "ページ",
"page-not-found": "ページが見つかりません",
+ "page-plural": "ページ",
"page-views-by-data-asset-plural": "データアセットごとのページビュー",
"parameter": "パラメータ",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "除外するカラムを選択",
"select-column-plural-to-include": "含めるカラムを選択",
"select-dimension": "ディメンションを選択",
+ "select-duration": "Select Duration",
"select-entity": "{{entity}} を選択",
"select-entity-type": "エンティティタイプを選択",
"select-field": "{{field}} を選択",
"select-field-and-weight": "フィールドと重みを選択",
+ "select-page-plural": "ページを選択",
"select-schema-object": "スキーマオブジェクトを選択",
+ "select-status": "Select Status",
"select-tag": "タグを選択",
"select-test-type": "テストタイプを選択",
"select-to-search": "検索して選択",
@@ -1675,6 +1693,7 @@
"source-label": "ソース:",
"source-match": "イベントソースで一致",
"source-plural": "ソース",
+ "source-provider": "ソースプロバイダー",
"source-url": "ソース URL",
"source-with-details": "ソース: {{source}} ({{entityName}})",
"specific-data-asset-plural": "特定のデータアセット",
@@ -1916,6 +1935,7 @@
"update-image": "画像を更新",
"update-request-tag-plural": "リクエストタグを更新",
"updated": "更新済み",
+ "updated-at": "Updated at",
"updated-by": "更新者",
"updated-lowercase": "更新済み",
"updated-on": "更新日",
@@ -2260,6 +2280,7 @@
"enter-a-field": "{{field}} を入力してください",
"enter-column-description": "カラムの説明を入力してください",
"enter-comma-separated-field": "{{field}} をカンマ区切りで入力してください",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "表示名を入力してください",
"enter-feature-description": "機能の説明を入力してください",
"enter-interval": "間隔を入力してください",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "外部の宛先のみテストできます。",
"failed-events-description": "特定アラートに対して失敗したイベントの件数",
"failed-status-for-entity-deploy": "<0>{{entity}}0> は {{entityStatus}} されましたが、デプロイに失敗しました",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "画像の読み込みに失敗しました",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{count}}件のチェック中{{failed}}件が失敗",
"feed-asset-action-header": "<0>データアセット0> を {{action}}",
"feed-custom-property-header": "カスタムプロパティを更新",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "大丈夫。目標を再構築して、より早く前進しましょう。",
"latency-sla-description": "<0>{{label}}0>: クエリ応答は<0>{{data}}0>未満である必要があります",
"latest-offset-description": "システム内の最新のイベントオフセットです。",
+ "learning-resources-management-description": "製品の機能を探索し、リソースを通じてその仕組みを学びましょう",
"leave-the-team-team-name": "チーム {{teamName}} から退出する",
"length-validator-error": "{{length}} 文字以上の {{field}} が必要です",
"lineage-ingestion-description": "メタデータの取り込み設定が完了した後、リネージ取り込みを構成・デプロイできます。リネージ取り込みワークフローでは、クエリ履歴を取得し、CREATE、INSERT、MERGE などのクエリを解析してエンティティ間のリネージを作成します。1つのデータベースサービスに対して、リネージ取り込みは1つのパイプラインのみ可能です。開始するには、クエリログの期間(日数)と結果の上限を定義してください。",
@@ -2512,6 +2536,7 @@
"no-kpi": "影響をモニターして、より良い意思決定のために KPI(主要業績評価指標)を定義してください。",
"no-kpi-available-add-new-one": "KPI が利用できません。「KPI を追加」ボタンをクリックしてください。",
"no-kpi-found": "名前 {{name}} に一致する KPI は見つかりませんでした",
+ "no-learning-resources-available": "このコンテキストで利用可能な学習リソースはありません。",
"no-lineage-available": "lineage の接続は見つかりませんでした",
"no-match-found": "一致するものは見つかりませんでした",
"no-mentions": "あなたやチームが参照されているアクティビティはありません",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "承認または却下できるのはレビュアーのみです",
"only-show-columns-with-lineage": "リネージを持つカラムのみを表示",
"optional-configuration-update-description-dbt": "dbt から説明文を更新するかどうかのオプション設定です",
+ "optional-markdown-content": "オプション:リソースに直接埋め込むマークダウンコンテンツを追加",
"owners-coverage-widget-description": "サービス内の全データアセットの所有者カバレッジ。<0>詳細を見る。0>",
"page-is-not-available": "お探しのページはご利用いただけません",
"page-sub-header-for-activity-feed": "データ変更イベントの概要を表示するアクティビティフィードです。",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "破棄",
"unsaved-changes-save": "変更を保存",
"unsaved-changes-warning": "保存されていない変更があります。閉じると破棄されます。",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "説明の更新をリクエスト中:",
"update-displayName-entity": "{{entity}}の表示名を更新",
"update-profiler-settings": "プロファイラー設定を更新",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "OpenMetadataへようこそ!",
"welcome-to-open-metadata": "{{brandName}}へようこそ!",
"would-like-to-start-adding-some": "いくつか追加してみませんか?",
+ "write-markdown-content": "マークダウンコンテンツをここに記入...",
"write-your-announcement-lowercase": "お知らせを入力してください",
"write-your-description": "説明を入力してください",
"write-your-text": "{{text}}を入力してください",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "メールアドレスの認証に成功しました",
"add-entity-error": "{{entity}}の追加中にエラーが発生しました!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "トークンの更新はAuth Provider {{provider}}ではサポートされていません。",
"can-not-renew-token-authentication-not-present": "IDトークンを更新できません。認証プロバイダーが存在しません。",
"column-fetch-error": "カラムのテストケース取得中にエラーが発生しました!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "ユーザー名またはパスワードが正しくありません。",
"join-team-error": "チーム参加中にエラーが発生しました!",
"join-team-success": "チームに正常に参加しました!",
+ "learning-resources-fetch-error": "学習リソースの取得エラー",
"leave-team-error": "チームからの退会中にエラーが発生しました!",
"leave-team-success": "チームから正常に退会しました!",
"no-application-schema-found": "{{appName}}のアプリケーションスキーマが見つかりません",
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 d72fee724560..b25acd9d7452 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
@@ -59,6 +59,7 @@
"add-new-field": "새 필드 추가",
"add-new-widget-plural": "새 위젯 추가",
"add-product": "제품 추가",
+ "add-resource": "Add Resource",
"add-row": "행 추가",
"add-suggestion": "제안 추가",
"add-term-boost": "용어 부스트 추가",
@@ -315,6 +316,8 @@
"container": "컨테이너",
"container-column": "컨테이너 열",
"container-plural": "컨테이너들",
+ "content-name": "Content Name",
+ "context": "컨텍스트",
"contract": "계약",
"contract-detail-plural": "계약 세부사항",
"contract-execution-history": "계약 실행 기록",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "용어 표시 이름 편집",
"edit-glossary-name": "용어 이름 편집",
"edit-name": "이름 편집",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "제안 편집",
"edit-widget": "위젯 편집",
"edit-widget-plural": "위젯 편집",
@@ -597,6 +601,7 @@
"embed-file-type": "{{fileType}} 삽입",
"embed-image": "이미지 삽입",
"embed-link": "링크 삽입",
+ "embedded-content": "임베드된 콘텐츠",
"enable": "활성화",
"enable-debug-log": "디버그 로그 활성화",
"enable-entity": "{{entity}} 활성화",
@@ -657,6 +662,7 @@
"entity-reference-plural": "엔티티 참조들",
"entity-reference-types": "엔티티 참조 유형들",
"entity-report-data": "엔티티 보고서 데이터",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}} 실행 중",
"entity-scheduled-to-run-value": "{{entity}} 가 {{value}} 에 실행되도록 예약됨",
"entity-service": "{{entity}} 서비스",
@@ -995,8 +1001,11 @@
"latest-offset": "최신 오프셋",
"layer": "계층",
"layer-plural": "계층들",
+ "learn-how-this-feature-works": "이 기능이 어떻게 작동하는지 알아보시겠습니까?",
"learn-more": "더 알아보기",
"learn-more-and-support": "더 알아보기 & 지원",
+ "learning-resource": "학습 리소스",
+ "learning-resources": "학습 리소스",
"leave-team": "팀 탈퇴",
"legend": "Legend",
"less": "적게",
@@ -1025,7 +1034,8 @@
"live": "실시간",
"load-more": "더 불러오기",
"loading": "로딩 중",
- "loading-graph": "그래프 로딩 중",
+ "loading-article": "Loading article...",
+ "loading-graph": "Loading Graph",
"local-config-source": "로컬 설정 소스",
"location": "위치",
"log-lowercase-plural": "logs",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "MIME 타입",
"min": "최소",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "마인드맵",
"minor": "부",
"minute": "분",
@@ -1123,6 +1135,7 @@
"month": "월",
"monthly": "월간",
"more": "더 보기",
+ "more-action": "More Action",
"more-help": "추가 도움말",
"more-lowercase": "더",
"most-active-user": "가장 활발한 사용자",
@@ -1244,6 +1257,7 @@
"open-metadata": "OpenMetadata",
"open-metadata-logo": "OpenMetadata 로고",
"open-metadata-url": "OpenMetadata URL",
+ "open-original": "Open Original",
"open-task-plural": "열린 작업들",
"operation": "작업",
"operation-plural": "작업들",
@@ -1273,6 +1287,7 @@
"ownership": "소유권",
"page": "페이지",
"page-not-found": "페이지를 찾을 수 없습니다",
+ "page-plural": "페이지",
"page-views-by-data-asset-plural": "데이터 자산별 페이지 조회수",
"parameter": "매개변수",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "제외할 열 선택",
"select-column-plural-to-include": "포함할 열 선택",
"select-dimension": "차원 선택",
+ "select-duration": "Select Duration",
"select-entity": "{{entity}} 선택",
"select-entity-type": "엔티티 유형 선택",
"select-field": "{{field}} 선택",
"select-field-and-weight": "필드 및 가중치 선택",
+ "select-page-plural": "페이지 선택",
"select-schema-object": "스키마 객체 선택",
+ "select-status": "Select Status",
"select-tag": "태그 선택",
"select-test-type": "테스트 유형 선택",
"select-to-search": "검색하여 선택",
@@ -1675,6 +1693,7 @@
"source-label": "Source:",
"source-match": "이벤트 소스로 매칭",
"source-plural": "소스들",
+ "source-provider": "소스 제공자",
"source-url": "소스 URL",
"source-with-details": "소스: {{source}} ({{entityName}})",
"specific-data-asset-plural": "특정 데이터 자산들",
@@ -1916,6 +1935,7 @@
"update-image": "이미지 업데이트",
"update-request-tag-plural": "요청 태그 업데이트",
"updated": "업데이트됨",
+ "updated-at": "Updated at",
"updated-by": "업데이트한 사람",
"updated-lowercase": "업데이트됨",
"updated-on": "업데이트 날짜",
@@ -2260,6 +2280,7 @@
"enter-a-field": "{{field}}을(를) 입력하세요",
"enter-column-description": "열 설명을 입력하세요",
"enter-comma-separated-field": "쉼표(,)로 구분된 {{field}}을(를) 입력하세요",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "표시 이름을 입력하세요",
"enter-feature-description": "기능 설명을 입력하세요",
"enter-interval": "간격을 입력하세요",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "외부 대상만 테스트할 수 있습니다.",
"failed-events-description": "특정 알림에 대한 실패 이벤트 수입니다.",
"failed-status-for-entity-deploy": "<0>{{entity}}0>이(가) {{entityStatus}}되었지만 배포에 실패했습니다",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "이미지 로드 실패",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{count}}개 중 {{failed}}개 실패",
"feed-asset-action-header": "<0>데이터 자산0> {{action}}",
"feed-custom-property-header": "사용자 정의 속성 업데이트됨:",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "알림: 설명 KPI 목표가 아직 달성되지 않았지만 시간이 남아 있습니다 - 조직에 {{count}}일이 남았습니다. 진행 상황을 유지하려면 데이터 인사이트 보고서를 활성화하세요. 이를 통해 모든 팀에 주간 업데이트를 보내 조직의 KPI 달성을 위한 협업과 집중을 촉진할 수 있습니다.",
"latency-sla-description": "<0>{{label}}0>: 쿼리 응답은 <0>{{data}}0> 보다 낮아야 합니다.",
"latest-offset-description": "시스템 내 이벤트의 최신 오프셋입니다.",
+ "learning-resources-management-description": "제품 기능을 탐색하고 리소스를 통해 작동 방식을 알아보세요",
"leave-the-team-team-name": "{{teamName}} 팀 떠나기",
"length-validator-error": "최소 {{length}}개의 {{field}}이(가) 필요합니다",
"lineage-ingestion-description": "계보 수집은 메타데이터 수집이 설정된 후 구성 및 배포할 수 있습니다. 계보 수집 워크플로우는 쿼리 기록을 가져와 CREATE, INSERT, MERGE... 쿼리를 파싱하고 관련 엔티티 간의 계보를 준비합니다. 계보 수집은 데이터베이스 서비스당 하나의 파이프라인만 가질 수 있습니다. 시작하려면 쿼리 로그 기간(일)과 결과 제한을 정의하세요.",
@@ -2512,6 +2536,7 @@
"no-kpi": "영향을 모니터링하고 더 스마트한 의사 결정을 내리기 위해 핵심 성과 지표를 정의하세요.",
"no-kpi-available-add-new-one": "사용 가능한 KPI가 없습니다. KPI 추가 버튼을 클릭하여 추가하세요.",
"no-kpi-found": "{{name}} 이름의 KPI를 찾을 수 없습니다",
+ "no-learning-resources-available": "이 컨텍스트에 사용 가능한 학습 리소스가 없습니다.",
"no-lineage-available": "lineage 연결을 찾을 수 없습니다",
"no-match-found": "일치하는 항목을 찾을 수 없습니다",
"no-mentions": "당신과 당신의 팀은 모두 괜찮아 보입니다 - 아직 어떤 활동에서도 언급되지 않았습니다. 계속 좋은 일을 하세요!",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "검토자만 승인 또는 거부할 수 있습니다",
"only-show-columns-with-lineage": "리니지가 있는 컬럼만 표시",
"optional-configuration-update-description-dbt": "dbt에서 설명을 업데이트할지 여부에 대한 선택적 구성",
+ "optional-markdown-content": "선택사항: 리소스에 직접 포함할 마크다운 콘텐츠 추가",
"owners-coverage-widget-description": "서비스 내 모든 데이터 자산의 소유자 커버리지입니다. <0>더 알아보기.0>",
"page-is-not-available": "찾고 있는 페이지를 사용할 수 없습니다",
"page-sub-header-for-activity-feed": "데이터 변경 이벤트의 요약을 볼 수 있는 활동 피드입니다.",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "취소",
"unsaved-changes-save": "변경 사항 저장",
"unsaved-changes-warning": "저장되지 않은 변경 사항이 있을 수 있으며, 닫으면 모두 삭제됩니다.",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "다음에 대한 설명 업데이트 요청:",
"update-displayName-entity": "{{entity}}의 표시 이름을 업데이트합니다.",
"update-profiler-settings": "프로파일러 설정을 업데이트합니다.",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "OpenMetadata에 오신 것을 환영합니다!",
"welcome-to-open-metadata": "{{brandName}}에 오신 것을 환영합니다!",
"would-like-to-start-adding-some": "일부를 추가하시겠습니까?",
+ "write-markdown-content": "마크다운 콘텐츠를 여기에 작성하세요...",
"write-your-announcement-lowercase": "공지사항 작성",
"write-your-description": "설명 작성",
"write-your-text": "{{text}} 작성",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "이메일이 성공적으로 인증되었습니다",
"add-entity-error": "{{entity}} 추가 중 오류가 발생했습니다!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "{{provider}} 인증 제공자는 토큰 갱신을 지원하지 않습니다.",
"can-not-renew-token-authentication-not-present": "ID 토큰을 갱신할 수 없습니다. 인증 제공자가 존재하지 않습니다.",
"column-fetch-error": "컬럼 테스트 케이스를 가져오는 중 오류가 발생했습니다!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "잘못된 사용자 이름이나 비밀번호를 입력했습니다.",
"join-team-error": "팀 가입 중 오류가 발생했습니다!",
"join-team-success": "팀에 성공적으로 가입했습니다!",
+ "learning-resources-fetch-error": "학습 리소스를 가져오는 중 오류 발생",
"leave-team-error": "팀 탈퇴 중 오류가 발생했습니다!",
"leave-team-success": "팀을 성공적으로 탈퇴했습니다!",
"no-application-schema-found": "{{appName}}에 대한 애플리케이션 스키마를 찾을 수 없습니다.",
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 7405fdef5675..7f096b74d2b7 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
@@ -59,6 +59,7 @@
"add-new-field": "नवीन फील्ड जोडा",
"add-new-widget-plural": "नवीन विजेट्स जोडा",
"add-product": "उत्पादन जोडा",
+ "add-resource": "Add Resource",
"add-row": "पंक्ति जोडा",
"add-suggestion": "प्रस्ताव जोडा",
"add-term-boost": "टर्म बूस्ट जोडा",
@@ -315,6 +316,8 @@
"container": "कंटेनर",
"container-column": "कंटेनर स्तंभ",
"container-plural": "कंटेनर",
+ "content-name": "Content Name",
+ "context": "संदर्भ",
"contract": "Contract",
"contract-detail-plural": "Contract Details",
"contract-execution-history": "करार चालवणी इतिहास",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "शब्दकोश प्रदर्शन नाव संपादित करा",
"edit-glossary-name": "शब्दकोश नाव संपादित करा",
"edit-name": "नाव संपादित करा",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "सूचना संपादित करा",
"edit-widget": "विजेट संपादित करा",
"edit-widget-plural": "विजेट्स संपादित करा",
@@ -597,6 +601,7 @@
"embed-file-type": "Embed {{fileType}}",
"embed-image": "प्रतिमा एम्बेड करा",
"embed-link": "लिंक एम्बेड करा",
+ "embedded-content": "एम्बेडेड सामग्री",
"enable": "सक्षम करा",
"enable-debug-log": "डिबग लॉग सक्षम करा",
"enable-entity": "{{entity}} सक्षम करा",
@@ -657,6 +662,7 @@
"entity-reference-plural": "घटक संदर्भ",
"entity-reference-types": "घटक संदर्भ प्रकार",
"entity-report-data": "एंटिटी रिपोर्ट डेटा",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}} चालू आहे",
"entity-scheduled-to-run-value": "{{entity}} {{value}} या वेळी चालविण्यासाठी नियोजित",
"entity-service": "{{entity}} सेवा",
@@ -995,8 +1001,11 @@
"latest-offset": "नवीनतम ऑफसेट",
"layer": "स्तर",
"layer-plural": "स्तर",
+ "learn-how-this-feature-works": "या वैशिष्ट्याचे कार्य कसे होते ते शिका?",
"learn-more": "अधिक जाणून घ्या",
"learn-more-and-support": "अधिक जाणून घ्या आणि समर्थन",
+ "learning-resource": "शिक्षण संसाधन",
+ "learning-resources": "शिक्षण संसाधने",
"leave-team": "टीम सोडा",
"legend": "Legend",
"less": "कमी",
@@ -1025,6 +1034,7 @@
"live": "थेट",
"load-more": "अधिक लोड करा",
"loading": "लोड करत आहे",
+ "loading-article": "Loading article...",
"loading-graph": "Loading Graph",
"local-config-source": "स्थानिक संरचना स्रोत",
"location": "स्थान",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "MIME प्रकार",
"min": "किमान",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "माइंड मॅप",
"minor": "लहान",
"minute": "मिनिट",
@@ -1123,6 +1135,7 @@
"month": "महिना",
"monthly": "महिन्याने",
"more": "अधिक",
+ "more-action": "More Action",
"more-help": "अधिक मदत",
"more-lowercase": "अधिक",
"most-active-user": "सर्वात सक्रिय वापरकर्ता",
@@ -1244,6 +1257,7 @@
"open-metadata": "ओपनमेटाडेटा",
"open-metadata-logo": "ओपनमेटाडेटा लोगो",
"open-metadata-url": "ओपनमेटाडेटा URL",
+ "open-original": "Open Original",
"open-task-plural": "उघडी कार्ये",
"operation": "Operation",
"operation-plural": "ऑपरेशन्स",
@@ -1273,6 +1287,7 @@
"ownership": "स्वामित्व",
"page": "Page",
"page-not-found": "पृष्ठ सापडले नाही",
+ "page-plural": "पृष्ठे",
"page-views-by-data-asset-plural": "डेटा ॲसेट्सने पृष्ठ दृश्ये",
"parameter": "पॅरामीटर",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "वगळण्यासाठी स्तंभ निवडा",
"select-column-plural-to-include": "समाविष्ट करण्यासाठी स्तंभ निवडा",
"select-dimension": "परिमाण निवडा",
+ "select-duration": "Select Duration",
"select-entity": "{{entity}} निवडा",
"select-entity-type": "एन्टिटी टाइप निवडा",
"select-field": "{{field}} निवडा",
"select-field-and-weight": "फील्ड आणि वजन निवडा",
+ "select-page-plural": "पृष्ठे निवडा",
"select-schema-object": "स्कीमा ऑब्जेक्ट निवडा",
+ "select-status": "Select Status",
"select-tag": "टॅग निवडा",
"select-test-type": "चाचणी प्रकार निवडा",
"select-to-search": "शोधण्यासाठी निवडा",
@@ -1675,6 +1693,7 @@
"source-label": "Source:",
"source-match": "इव्हेंट स्रोताद्वारे जुळवा",
"source-plural": "स्रोत",
+ "source-provider": "स्त्रोत प्रदाता",
"source-url": "स्रोत URL",
"source-with-details": "Source: {{source}} ({{entityName}})",
"specific-data-asset-plural": "विशिष्ट डेटा ॲसेट",
@@ -1916,6 +1935,7 @@
"update-image": "प्रतिमा अद्यतनित करा",
"update-request-tag-plural": "अद्यतनित विनंती टॅग्ज",
"updated": "अद्यतनित केले",
+ "updated-at": "Updated at",
"updated-by": "द्वारे अद्यतनित केले",
"updated-lowercase": "अद्यतनित केले",
"updated-on": "अद्यतनित केले",
@@ -2260,6 +2280,7 @@
"enter-a-field": "{{field}} प्रविष्ट करा",
"enter-column-description": "स्तंभ वर्णन प्रविष्ट करा",
"enter-comma-separated-field": "स्वल्पविराम(,) विभक्त {{field}} प्रविष्ट करा",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "प्रदर्शन नाव प्रविष्ट करा",
"enter-feature-description": "वैशिष्ट्य वर्णन प्रविष्ट करा",
"enter-interval": "अंतराल प्रविष्ट करा",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "फक्त बाह्य गंतव्ये चाचणी केली जाऊ शकतात.",
"failed-events-description": "Count of failed events for specific alert.",
"failed-status-for-entity-deploy": "<0>{{entity}}0> {{entityStatus}} केले गेले आहे, परंतु उपयोजन अयशस्वी झाले",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "प्रतिमा लोड करण्यात अयशस्वी",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{count}} तपासण्यांपैकी {{failed}} अयशस्वी",
"feed-asset-action-header": "{{action}} <0>डेटा ॲसेट0>",
"feed-custom-property-header": "सानुकूल गुणधर्म अद्यतनित केले",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "सूचना: वर्णन KPI लक्ष्य अद्याप पूर्ण झालेले नाही, परंतु अजूनही वेळ आहे – तुमच्या संस्थेकडे {{count}} दिवस शिल्लक आहेत. ट्रॅकवर राहण्यासाठी, कृपया डेटा अंतर्दृष्टी अहवाल सक्षम करा. हे आम्हाला सर्व टीम्सना साप्ताहिक अद्यतने पाठविण्याची परवानगी देईल, सहकार्य आणि आमच्या संस्थेच्या KPI साध्य करण्याच्या दिशेने लक्ष केंद्रित करेल.",
"latency-sla-description": "<0>{{label}}0>: Query response must be under <0>{{data}}0>",
"latest-offset-description": "The latest offset of the event in the system.",
+ "learning-resources-management-description": "उत्पादन वैशिष्ट्ये एक्सप्लोर करा आणि आमच्या संसाधनांद्वारे ते कसे कार्य करतात ते शिका",
"leave-the-team-team-name": "टीम {{teamName}} सोडा",
"length-validator-error": "किमान {{length}} {{field}} आवश्यक",
"lineage-ingestion-description": "वंशावळ अंतर्ग्रहण मेटाडेटा अंतर्ग्रहण सेट केल्यानंतर कॉन्फिगर आणि तैनात केले जाऊ शकते. वंशावळ अंतर्ग्रहण कार्यप्रवाह क्वेरी इतिहास प्राप्त करतो, CREATE, INSERT, MERGE... क्वेरी पार्स करतो आणि संबंधित घटकांमधील वंशावळ तयार करतो. वंशावळ अंतर्ग्रहणासाठी डेटाबेस सेवेसाठी फक्त एक पाइपलाइन असू शकते. प्रारंभ करण्यासाठी क्वेरी लॉग कालावधी (दिवसांत) आणि परिणाम मर्यादा परिभाषित करा.",
@@ -2512,6 +2536,7 @@
"no-kpi": "प्रभावाचे निरीक्षण करण्यासाठी व अधिक हुशार निर्णय घेण्यासाठी प्रमुख प्रदर्शन निर्देशांक (KPI) परिभाषित करा.",
"no-kpi-available-add-new-one": "कोणतेही KPI उपलब्ध नाहीत. एक जोडण्यासाठी Add KPI बटणावर क्लिक करा.",
"no-kpi-found": "{{name}} नावाने कोणतेही KPI सापडले नाही",
+ "no-learning-resources-available": "या संदर्भासाठी कोणतीही शिक्षण संसाधने उपलब्ध नाहीत.",
"no-lineage-available": "कोणतेही lineage कनेक्शन सापडले नाहीत",
"no-match-found": "कोणतेही जुळणारे सापडले नाही",
"no-mentions": "असे दिसते की तुम्ही आणि तुमची टीम सर्व स्पष्ट आहात – कोणत्याही क्रियाकलापांमध्ये कोणतेही उल्लेख नाहीत. उत्तम काम करत रहा!",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "फक्त पुनरावलोकक मंजूर किंवा नाकारू शकतात",
"only-show-columns-with-lineage": "केवळ Lineage असलेले स्तंभ दाखवा",
"optional-configuration-update-description-dbt": "dbt मधून वर्णन अद्यतनित करण्यासाठी पर्यायी संरचना",
+ "optional-markdown-content": "पर्यायी: संसाधनात थेट एम्बेड करण्यासाठी markdown सामग्री जोडा",
"owners-coverage-widget-description": "सेवेतील सर्व डेटा मालमत्तांसाठी मालक व्याप्ती. <0>अधिक जाणून घ्या.0>",
"page-is-not-available": "तुम्ही शोधत असलेले पृष्ठ उपलब्ध नाही",
"page-sub-header-for-activity-feed": "डेटा बदल इव्हेंट्सचा सारांश पाहण्यासाठी क्रियाकलाप फीड.",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "टाकून द्या",
"unsaved-changes-save": "बदल सेव्ह करा",
"unsaved-changes-warning": "तुमच्याकडे जतन न केलेले बदल असू शकतात जे बंद केल्यावर रद्द केले जातील.",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "साठी वर्णन अद्यतनित करण्याची विनंती",
"update-displayName-entity": "{{entity}} साठी प्रदर्शन नाव अद्यतनित करा.",
"update-profiler-settings": "प्रोफाइलर सेटिंग अद्यतनित करा.",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "OpenMetadata मध्ये स्वागत आहे!",
"welcome-to-open-metadata": "{{brandName}} मध्ये स्वागत आहे!",
"would-like-to-start-adding-some": "काही जोडण्यास प्रारंभ करू इच्छिता?",
+ "write-markdown-content": "येथे markdown सामग्री लिहा...",
"write-your-announcement-lowercase": "तुमची घोषणा लिहा",
"write-your-description": "तुमचे वर्णन लिहा",
"write-your-text": "तुमचा {{text}} लिहा",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "ईमेल यशस्वीरित्या सत्यापित केले",
"add-entity-error": "{{entity}} जोडताना त्रुटी!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "प्रमाणीकरण प्रदाता {{provider}} टोकन नूतनीकरणासाठी समर्थित नाही.",
"can-not-renew-token-authentication-not-present": "id टोकन नूतनीकरण करू शकत नाही. प्रमाणीकरण प्रदाता उपस्थित नाही.",
"column-fetch-error": "स्तंभ चाचणी प्रकरण मिळवताना त्रुटी!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "तुम्ही अवैध वापरकर्तानाव किंवा पासवर्ड प्रविष्ट केला आहे.",
"join-team-error": "टीममध्ये सामील होताना त्रुटी!",
"join-team-success": "टीममध्ये यशस्वीरित्या सामील झाले!",
+ "learning-resources-fetch-error": "शिक्षण संसाधने आणण्यात त्रुटी",
"leave-team-error": "टीम सोडताना त्रुटी!",
"leave-team-success": "टीम यशस्वीरित्या सोडली!",
"no-application-schema-found": "{{appName}} साठी अनुप्रयोग स्कीमा सापडले नाही",
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 285a61903dfa..e9777ae1dcad 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
@@ -59,6 +59,7 @@
"add-new-field": "Nieuw veld toevoegen",
"add-new-widget-plural": "Nieuwe widgets toevoegen",
"add-product": "Product toevoegen",
+ "add-resource": "Add Resource",
"add-row": "Rij toevoegen",
"add-suggestion": "Suggestie toevoegen",
"add-term-boost": "Term Boost toevoegen",
@@ -315,6 +316,8 @@
"container": "Container",
"container-column": "Container Column",
"container-plural": "Containers",
+ "content-name": "Content Name",
+ "context": "Context",
"contract": "Contract",
"contract-detail-plural": "Contract details",
"contract-execution-history": "Contractuitvoeringsgeschiedenis",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "Weergavenaam van het woordenboek bewerken",
"edit-glossary-name": "Naam van het woordenboek bewerken",
"edit-name": "Naam bewerken",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "Suggestie bewerken",
"edit-widget": "Widget bewerken",
"edit-widget-plural": "Widgets bewerken",
@@ -597,6 +601,7 @@
"embed-file-type": "Embed {{fileType}}",
"embed-image": "Afbeelding insluiten",
"embed-link": "Koppeling insluiten",
+ "embedded-content": "Ingesloten Inhoud",
"enable": "Inschakelen",
"enable-debug-log": "Debuglog inschakelen",
"enable-entity": "{{entity}} inschakelen",
@@ -657,6 +662,7 @@
"entity-reference-plural": "Entity References",
"entity-reference-types": "Entity Reference Types",
"entity-report-data": "Entity Report Data",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}} Draait",
"entity-scheduled-to-run-value": "{{entity}} gepland om uit te voeren {{value}}",
"entity-service": "{{entity}}-service",
@@ -995,8 +1001,11 @@
"latest-offset": "Laatste offset",
"layer": "Layer",
"layer-plural": "Layers",
+ "learn-how-this-feature-works": "Leer hoe deze functie werkt?",
"learn-more": "Learn More",
"learn-more-and-support": "Meer leren & Ondersteuning",
+ "learning-resource": "Leermiddel",
+ "learning-resources": "Leermiddelen",
"leave-team": "Team verlaten",
"legend": "Legend",
"less": "Minder",
@@ -1025,7 +1034,8 @@
"live": "Live",
"load-more": "Meer laden",
"loading": "Laden",
- "loading-graph": "Grafiek laden",
+ "loading-article": "Loading article...",
+ "loading-graph": "Loading Graph",
"local-config-source": "Lokale configuratiebron",
"location": "Locatie",
"log-lowercase-plural": "logs",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "MIME-type",
"min": "Min",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "Mindmap",
"minor": "Klein",
"minute": "Minuut",
@@ -1123,6 +1135,7 @@
"month": "Maand",
"monthly": "Maandelijks",
"more": "Meer",
+ "more-action": "More Action",
"more-help": "Meer hulp",
"more-lowercase": "meer",
"most-active-user": "Meest actieve gebruiker",
@@ -1244,6 +1257,7 @@
"open-metadata": "OpenMetadata",
"open-metadata-logo": "OpenMetadata-logo",
"open-metadata-url": "OpenMetadata URL",
+ "open-original": "Open Original",
"open-task-plural": "Open Tasks",
"operation": "Bewerking",
"operation-plural": "Bewerkingen",
@@ -1273,6 +1287,7 @@
"ownership": "Eigendom",
"page": "Pagina",
"page-not-found": "Pagina niet gevonden",
+ "page-plural": "Pagina's",
"page-views-by-data-asset-plural": "Paginaweergaven per data-asset",
"parameter": "Parameter",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "Selecteer kolommen om uit te sluiten",
"select-column-plural-to-include": "Selecteer kolommen om op te nemen",
"select-dimension": "Selecteer dimensie",
+ "select-duration": "Select Duration",
"select-entity": "Select {{entity}}",
"select-entity-type": "Entiteitstype selecteren",
"select-field": "Selecteer {{field}}",
"select-field-and-weight": "Veld en gewicht selecteren",
+ "select-page-plural": "Pagina's selecteren",
"select-schema-object": "Selecteer schema-object",
+ "select-status": "Select Status",
"select-tag": "Selecteer een tag",
"select-test-type": "Selecteer testtype",
"select-to-search": "Zoeken om te selecteren",
@@ -1675,6 +1693,7 @@
"source-label": "Source:",
"source-match": "Overeenkomst op eventbron",
"source-plural": "Bronnen",
+ "source-provider": "Bronprovider",
"source-url": "Bron-URL",
"source-with-details": "Bron: {{source}} ({{entityName}})",
"specific-data-asset-plural": "Specifieke data-assets",
@@ -1916,6 +1935,7 @@
"update-image": "Afbeelding updaten",
"update-request-tag-plural": "Updateverzoek tags",
"updated": "Geüpdatet",
+ "updated-at": "Updated at",
"updated-by": "Geüpdatet door",
"updated-lowercase": "geüpdatet",
"updated-on": "Geüpdatet op",
@@ -2260,6 +2280,7 @@
"enter-a-field": "Voer een {{field}} in",
"enter-column-description": "Voer kolombeschrijving in",
"enter-comma-separated-field": "Voer komma(,) gescheiden {{field}} in",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "Voer weergavenaam in",
"enter-feature-description": "Voer functiebeschrijving in",
"enter-interval": "Voer interval in",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "Alleen externe bestemmingen kunnen worden getest.",
"failed-events-description": "Aantal mislukte gebeurtenissen voor specifieke alert.",
"failed-status-for-entity-deploy": "<0>{{entity}}0> is {{entityStatus}}, maar het deployen is mislukt",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "Laden van afbeelding mislukt",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{failed}} mislukt van de {{count}} controles",
"feed-asset-action-header": "{{action}} <0>data asset0>",
"feed-custom-property-header": "updated Custom Properties on",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "Maakt niet uit. Het is tijd om je doelen te herstructureren en sneller vooruitgang te boeken.",
"latency-sla-description": "<0>{{label}}0>: Reactietijd van de query moet onder <0>{{data}}0> liggen.",
"latest-offset-description": "De laatste offset van de gebeurtenis in het systeem.",
+ "learning-resources-management-description": "Ontdek productfuncties en leer hoe ze werken via onze bronnen",
"leave-the-team-team-name": "Verlaat het team {{teamName}}",
"length-validator-error": "Minimaal {{length}} {{field}} vereist",
"lineage-ingestion-description": "Lineage-ingestie kan worden geconfigureerd en ingezet nadat een metadataingestie is ingesteld. De lineage-ingestieworkflow verkrijgt de querygeschiedenis, analyseert CREATE, INSERT, MERGE... queries en bereidt de lineage voor tussen de betrokken entiteiten. Lineage-ingestie kan slechts één pipeline hebben voor een databaseservice. Definieer de duur van de querylog (in dagen) en het resultaatlimiet om te beginnen.",
@@ -2512,6 +2536,7 @@
"no-kpi": "Definieer prestatie-indicatoren om impact te volgen en slimmere beslissingen te nemen.",
"no-kpi-available-add-new-one": "Er zijn geen KPI's beschikbaar. Klik op de knop KPI toevoegen om er een toe te voegen.",
"no-kpi-found": "Geen KPI gevonden met de naam {{name}}",
+ "no-learning-resources-available": "Geen leermiddelen beschikbaar voor deze context.",
"no-lineage-available": "Geen lineage-connecties gevonden",
"no-match-found": "Geen overeenkomst gevonden",
"no-mentions": "Er zijn geen gevallen waarin jij of jouw team zijn genoemd in activiteiten",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "Alleen reviewers kunnen goedkeuren of afwijzen",
"only-show-columns-with-lineage": "Toon alleen kolommen met Lineage",
"optional-configuration-update-description-dbt": "Optionele configuratie om de beschrijving van dbt eventueel bij te werken",
+ "optional-markdown-content": "Optioneel: Voeg markdown-inhoud toe om direct in de bron in te sluiten",
"owners-coverage-widget-description": "Eigenaarsdekking voor alle data-assets in de service. <0>meer informatie.0>",
"page-is-not-available": "De pagina die je zoekt is niet beschikbaar",
"page-sub-header-for-activity-feed": "Activiteitenfeed waarmee je een samenvatting van data-wijzigingsevents kunt bekijken.",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "Verwijderen",
"unsaved-changes-save": "Wijzigingen opslaan",
"unsaved-changes-warning": "Je hebt mogelijk niet-opgeslagen wijzigingen die worden verwijderd bij het sluiten.",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "Verzoek om de beschrijving aan te passen voor",
"update-displayName-entity": "Update de weergavenaam voor de {{entity}}.",
"update-profiler-settings": "Profielinstellingen updaten.",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "Welkom bij OpenMetadata!",
"welcome-to-open-metadata": "Welkom bij {{brandName}}!",
"would-like-to-start-adding-some": "Wil je beginnen met toevoegen?",
+ "write-markdown-content": "Schrijf markdown-inhoud hier...",
"write-your-announcement-lowercase": "schrijf je aankondiging",
"write-your-description": "Schrijf je beschrijving",
"write-your-text": "Schrijf je {{text}}",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "E-mail succesvol geverifieerd",
"add-entity-error": "Fout bij het toevoegen van {{entity}}!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "Auth-provider {{provider}} wordt niet ondersteund voor het vernieuwen van tokens.",
"can-not-renew-token-authentication-not-present": "Kan id-token niet vernieuwen. Authenticatieprovider is niet aanwezig.",
"column-fetch-error": "Fout bij het ophalen van kolomtestgeval!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "U heeft een ongeldige gebruikersnaam of wachtwoord ingevoerd.",
"join-team-error": "Fout bij het toetreden tot het team!",
"join-team-success": "Team succesvol toegetreden!",
+ "learning-resources-fetch-error": "Fout bij ophalen leermiddelen",
"leave-team-error": "Fout bij het verlaten van het team!",
"leave-team-success": "Team succesvol verlaten!",
"no-application-schema-found": "Geen applicatieschema gevonden voor {{appName}}",
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 fb6171ad7e20..5da40cfebd2b 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
@@ -59,6 +59,7 @@
"add-new-field": "افزودن فیلد جدید",
"add-new-widget-plural": "Add New Widgets",
"add-product": "اضافه کردن محصول",
+ "add-resource": "Add Resource",
"add-row": "Add Row",
"add-suggestion": "اضافه کردن پیشنهاد",
"add-term-boost": "تقویت مترادف اضافه کنید",
@@ -315,6 +316,8 @@
"container": "ظرف",
"container-column": "ستون ظرف",
"container-plural": "ظرفها",
+ "content-name": "Content Name",
+ "context": "زمینه",
"contract": "Contract",
"contract-detail-plural": "Contract Details",
"contract-execution-history": "تاریخچه اجرای قرارداد",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "ویرایش نام نمایشی واژهنامه",
"edit-glossary-name": "ویرایش نام واژهنامه",
"edit-name": "ویرایش نام",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "Editar sugerencia",
"edit-widget": "ویرایش ویجت",
"edit-widget-plural": "ویرایش ویجتها",
@@ -597,6 +601,7 @@
"embed-file-type": "Embed {{fileType}}",
"embed-image": "قرار دادن تصویر",
"embed-link": "قرار دادن لینک",
+ "embedded-content": "Embedded Content",
"enable": "فعال کردن",
"enable-debug-log": "فعال کردن گزارش اشکالزدایی",
"enable-entity": "فعال کردن {{entity}}",
@@ -657,6 +662,7 @@
"entity-reference-plural": "مراجع نهاد",
"entity-reference-types": "انواع مرجع نهاد",
"entity-report-data": "دادههای گزارش موجودیت",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}} در حال اجرا",
"entity-scheduled-to-run-value": "{{entity}} برای اجرا در {{value}} زمانبندی شده",
"entity-service": "سرویس {{entity}}",
@@ -995,8 +1001,11 @@
"latest-offset": "آخرین آفست",
"layer": "Layer",
"layer-plural": "لایهها",
+ "learn-how-this-feature-works": "یاد بگیرید این ویژگی چگونه کار میکند؟",
"learn-more": "بیشتر بدانید",
"learn-more-and-support": "بیشتر بدانید و پشتیبانی کنید",
+ "learning-resource": "Treasure of Knowledge",
+ "learning-resources": "Treasures of Knowledge",
"leave-team": "ترک تیم",
"legend": "Legend",
"less": "کمتر",
@@ -1025,6 +1034,7 @@
"live": "زنده",
"load-more": "بارگذاری بیشتر",
"loading": "در حال بارگذاری",
+ "loading-article": "Loading article...",
"loading-graph": "Loading Graph",
"local-config-source": "منبع تنظیمات محلی",
"location": "موقعیت",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "نوع MIME",
"min": "حداقل",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "نقشه ذهنی",
"minor": "فرعی",
"minute": "دقیقه",
@@ -1123,6 +1135,7 @@
"month": "ماه",
"monthly": "ماهانه",
"more": "بیشتر",
+ "more-action": "More Action",
"more-help": "کمک بیشتر",
"more-lowercase": "بیشتر",
"most-active-user": "فعالترین کاربر",
@@ -1244,6 +1257,7 @@
"open-metadata": "متادیتای باز",
"open-metadata-logo": "لوگوی متادیتای باز",
"open-metadata-url": "آدرس متادیتای باز",
+ "open-original": "Open Original",
"open-task-plural": "وظایف باز",
"operation": "Operation",
"operation-plural": "عملیات",
@@ -1273,6 +1287,7 @@
"ownership": "مالکیت",
"page": "Page",
"page-not-found": "صفحه یافت نشد",
+ "page-plural": "صفحات",
"page-views-by-data-asset-plural": "بازدیدهای صفحه توسط داراییهای داده",
"parameter": "پارامتر",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "انتخاب ستونها برای حذف",
"select-column-plural-to-include": "انتخاب ستونها برای اضافه کردن",
"select-dimension": "انتخاب بُعد",
+ "select-duration": "Select Duration",
"select-entity": "انتخاب {{entity}}",
"select-entity-type": "نوع جسم را انتخاب کنید",
"select-field": "انتخاب {{field}}",
"select-field-and-weight": "فیلد و وزن را انتخاب کنید",
+ "select-page-plural": "انتخاب صفحات",
"select-schema-object": "انتخاب شیء طرحواره",
+ "select-status": "Select Status",
"select-tag": "یک برچسب انتخاب کنید",
"select-test-type": "انتخاب نوع آزمون",
"select-to-search": "جستجو برای انتخاب",
@@ -1675,6 +1693,7 @@
"source-label": "Source:",
"source-match": "مطابقت بر اساس منبع رویداد",
"source-plural": "منابع",
+ "source-provider": "Source Provider",
"source-url": "آدرس منبع",
"source-with-details": "Source: {{source}} ({{entityName}})",
"specific-data-asset-plural": "داراییهای داده خاص",
@@ -1916,6 +1935,7 @@
"update-image": "بهروزرسانی تصویر",
"update-request-tag-plural": "بهروزرسانی برچسبهای درخواست",
"updated": "بهروزرسانی شده",
+ "updated-at": "Updated at",
"updated-by": "بهروزرسانی شده توسط",
"updated-lowercase": "بهروزرسانی شده",
"updated-on": "بهروزرسانی شده در",
@@ -2260,6 +2280,7 @@
"enter-a-field": "یک {{field}} وارد کنید.",
"enter-column-description": "توضیحات ستون را وارد کنید.",
"enter-comma-separated-field": "{{field}} با کاما (,) جداشده وارد کنید.",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "نام نمایشی را وارد کنید.",
"enter-feature-description": "توضیحات ویژگی را وارد کنید.",
"enter-interval": "فاصله زمانی را وارد کنید.",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "Only external destinations can be tested.",
"failed-events-description": "Count of failed events for specific alert.",
"failed-status-for-entity-deploy": "<0>{{entity}}0> با موفقیت {{entityStatus}} شد، اما پیادهسازی ناموفق بود.",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "Failed to load image",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{failed}} از {{count}} بررسی ناموفق",
"feed-asset-action-header": "{{action}} <0>دارایی داده0>",
"feed-custom-property-header": "بروزرسانی ویژگیهای سفارشی روی",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "توجه: هدف KPI هنوز محقق نشده است، اما هنوز زمان باقیست – سازمان شما {{count}} روز فرصت دارد. برای حفظ مسیر، لطفاً گزارش بینش داده را فعال کنید. این امکان را برای ما فراهم میکند که بهروزرسانیهای هفتگی را به تمامی تیمها ارسال کنیم تا همکاری و تمرکز به سمت دستیابی به KPIهای سازمانی حفظ شود.",
"latency-sla-description": "<0>{{label}}0>: پاسخ پرسوجو باید کمتر از <0>{{data}}0> باشد.",
"latest-offset-description": "The latest offset of the event in the system.",
+ "learning-resources-management-description": "Explore the ship's features and learn how they work through our treasure maps",
"leave-the-team-team-name": "تیم {{teamName}} را ترک کنید.",
"length-validator-error": "حداقل {{length}} {{field}} لازم است.",
"lineage-ingestion-description": "استخراج lineage میتواند پس از تنظیم یک فرآیند استخراج متادیتا پیکربندی و پیادهسازی شود. جریان کاری استخراج lineage تاریخچه درخواستها را بهدست میآورد، درخواستهای CREATE، INSERT، MERGE و... را تجزیه و تحلیل میکند و lineage بین موجودیتهای دخیل را آماده میکند. فرآیند استخراج lineage میتواند فقط یک لوله داده برای یک سرویس پایگاه داده داشته باشد. مدت زمان لاگ درخواست و محدودیت نتیجه را تعیین کنید تا شروع شود.",
@@ -2512,6 +2536,7 @@
"no-kpi": "برای نظارت بر تأثیر و تصمیمگیری هوشمندانهتر، شاخصهای کلیدی عملکرد (KPI) را تعریف کنید.",
"no-kpi-available-add-new-one": "هیچ KPIای موجود نیست. برای افزودن یک KPI روی دکمه Add KPI کلیک کنید.",
"no-kpi-found": "هیچ KPIای با نام {{name}} یافت نشد.",
+ "no-learning-resources-available": "Arrr! No treasures of knowledge be found fer this here context.",
"no-lineage-available": "هیچ اتصال lineage یافت نشد",
"no-match-found": "هیچ تطبیقی یافت نشد.",
"no-mentions": "به نظر میرسد شما و تیمتان در وضعیت خوبی هستید – هنوز هیچ ذکری در فعالیتها نیست. به کار خوبتان ادامه دهید!",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "فقط بازبینان میتوانند تأیید یا رد کنند.",
"only-show-columns-with-lineage": "فقط ستونهایی با Lineage را نمایش بده",
"optional-configuration-update-description-dbt": "پیکربندی اختیاری برای بهروزرسانی توضیحات از dbt یا خیر.",
+ "optional-markdown-content": "Optional: Add markdown content to embed directly in the resource",
"owners-coverage-widget-description": "پوشش مالکان برای تمام داراییهای داده در سرویس. <0>بیشتر بدانید.0>",
"page-is-not-available": "صفحهای که به دنبال آن هستید در دسترس نیست.",
"page-sub-header-for-activity-feed": "فید فعالیت که به شما امکان میدهد خلاصهای از رویدادهای تغییر داده را مشاهده کنید.",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "Descartar",
"unsaved-changes-save": "Salvar alterações",
"unsaved-changes-warning": "ممکن است تغییرات ذخیره نشدهای داشته باشید که با بستن از بین خواهند رفت.",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "درخواست برای بهروزرسانی توضیحات برای",
"update-displayName-entity": "بهروزرسانی نام نمایشی برای {{entity}}.",
"update-profiler-settings": "بهروزرسانی تنظیمات پروفایلر.",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "به OpenMetadata خوش آمدید!",
"welcome-to-open-metadata": "به {{brandName}} خوش آمدید!",
"would-like-to-start-adding-some": "آیا مایلید شروع به افزودن برخی کنید؟",
+ "write-markdown-content": "Write markdown content here...",
"write-your-announcement-lowercase": "اعلان خود را بنویسید",
"write-your-description": "توضیحات خود را بنویسید",
"write-your-text": "نوشتن {{text}} خود",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "ایمیل با موفقیت تأیید شد.",
"add-entity-error": "خطا در افزودن {{entity}}!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "ارائهدهنده احراز هویت {{provider}} برای تجدید توکن پشتیبانی نمیشود.",
"can-not-renew-token-authentication-not-present": "عدم توانایی در تجدید توکن id. ارائهدهنده احراز هویت موجود نیست.",
"column-fetch-error": "خطا در بازیابی آزمون ستون!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "نام کاربری یا رمز عبور نامعتبر است.",
"join-team-error": "خطا در پیوستن به تیم!",
"join-team-success": "با موفقیت به تیم پیوستید!",
+ "learning-resources-fetch-error": "Shiver me timbers! Error fetchin' the treasures",
"leave-team-error": "خطا در ترک تیم!",
"leave-team-success": "با موفقیت از تیم خارج شدید!",
"no-application-schema-found": "هیچ طرح برنامهای برای {{appName}} یافت نشد",
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 1d5fb1c6358a..c0d0e7fc9a19 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
@@ -59,6 +59,7 @@
"add-new-field": "Adicionar Novo Campo",
"add-new-widget-plural": "Adicionar novos widgets",
"add-product": "Adicionar Produto",
+ "add-resource": "Add Resource",
"add-row": "Adicionar linha",
"add-suggestion": "Adicionar sugestão",
"add-term-boost": "Adicionar Impulso de Termo",
@@ -315,6 +316,8 @@
"container": "Contêiner",
"container-column": "Coluna de contêiner",
"container-plural": "Contêineres",
+ "content-name": "Content Name",
+ "context": "Contexto",
"contract": "Contrato",
"contract-detail-plural": "Detalhes do Contrato",
"contract-execution-history": "Histórico de execução do contrato",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "Editar Nome de Exibição do Glossário",
"edit-glossary-name": "Editar Nome do Glossário",
"edit-name": "Editar nome",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "Editar sugestão",
"edit-widget": "Editar widget",
"edit-widget-plural": "Editar Widgets",
@@ -597,6 +601,7 @@
"embed-file-type": "Incorporar {{fileType}}",
"embed-image": "Incorporar imagem",
"embed-link": "Incorporar link",
+ "embedded-content": "Conteúdo Incorporado",
"enable": "Habilitar",
"enable-debug-log": "Habilitar Log de Depuração",
"enable-entity": "Habilitar {{entity}}",
@@ -657,6 +662,7 @@
"entity-reference-plural": "Referências de entidades",
"entity-reference-types": "Tipos de referência de entidades",
"entity-report-data": "Dados de Relatório de Entidade",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}} Executando",
"entity-scheduled-to-run-value": "{{entity}} agendado para executar {{value}}",
"entity-service": "Serviço de {{entity}}",
@@ -995,8 +1001,11 @@
"latest-offset": "Último deslocamento",
"layer": "Camada",
"layer-plural": "Camadas",
+ "learn-how-this-feature-works": "Aprenda como esse recurso funciona?",
"learn-more": "Saber mais",
"learn-more-and-support": "Saiba mais e Suporte",
+ "learning-resource": "Recurso de aprendizagem",
+ "learning-resources": "Recursos de aprendizagem",
"leave-team": "Deixar o Time",
"legend": "Legend",
"less": "Menos",
@@ -1025,7 +1034,8 @@
"live": "Ao Vivo",
"load-more": "Carregar Mais",
"loading": "Carregando",
- "loading-graph": "A carregar gráfico",
+ "loading-article": "Loading article...",
+ "loading-graph": "Loading Graph",
"local-config-source": "Fonte de Configuração Local",
"location": "Localização",
"log-lowercase-plural": "logs",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "Tipo MIME",
"min": "Min",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "Mapa Mental",
"minor": "Menor",
"minute": "Minuto",
@@ -1123,6 +1135,7 @@
"month": "Mês",
"monthly": "Mensal",
"more": "Mais",
+ "more-action": "More Action",
"more-help": "Mais Ajuda",
"more-lowercase": "mais",
"most-active-user": "Usuário Mais Ativo",
@@ -1244,6 +1257,7 @@
"open-metadata": "OpenMetadata",
"open-metadata-logo": "Logo OpenMetadata",
"open-metadata-url": "URL do OpenMetadata",
+ "open-original": "Open Original",
"open-task-plural": "Tarefas abertas",
"operation": "Operation",
"operation-plural": "Operações",
@@ -1273,6 +1287,7 @@
"ownership": "Propriedade",
"page": "Página",
"page-not-found": "Página Não Encontrada",
+ "page-plural": "Páginas",
"page-views-by-data-asset-plural": "Visualizações de Página por Ativos de Dados",
"parameter": "Parâmetro",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "Selecionar Colunas para Excluir",
"select-column-plural-to-include": "Selecionar Colunas para Incluir",
"select-dimension": "Selecionar dimensão",
+ "select-duration": "Select Duration",
"select-entity": "Selecione {{entity}}",
"select-entity-type": "Selecionar tipo de entidade",
"select-field": "Selecionar {{field}}",
"select-field-and-weight": "Selecionar campo e peso",
+ "select-page-plural": "Selecionar páginas",
"select-schema-object": "Selecionar objeto de esquema",
+ "select-status": "Select Status",
"select-tag": "Selecione uma tag",
"select-test-type": "Selecionar tipo de teste",
"select-to-search": "Pesquisar para Selecionar",
@@ -1675,6 +1693,7 @@
"source-label": "Source:",
"source-match": "Correspondência por Fonte de Evento",
"source-plural": "Fontes",
+ "source-provider": "Provedor de Origem",
"source-url": "URL de origem",
"source-with-details": "Fonte: {{source}} ({{entityName}})",
"specific-data-asset-plural": "Ativos de Dados Específicos",
@@ -1916,6 +1935,7 @@
"update-image": "Atualizar Imagem",
"update-request-tag-plural": "Atualizar Tags de Solicitação",
"updated": "Atualizado",
+ "updated-at": "Updated at",
"updated-by": "Atualizado por",
"updated-lowercase": "atualizado",
"updated-on": "Atualizado em",
@@ -2260,6 +2280,7 @@
"enter-a-field": "Insira um {{field}}",
"enter-column-description": "Insira a descrição da coluna",
"enter-comma-separated-field": "Insira {{field}} separados por vírgula(,)",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "Insira o nome para exibição",
"enter-feature-description": "Insira a descrição do recurso",
"enter-interval": "Insira o intervalo",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "Somente destinos externos podem ser testados.",
"failed-events-description": "Contagem de eventos com falha para um alerta específico.",
"failed-status-for-entity-deploy": "<0>{{entity}}0> foi {{entityStatus}}, mas falhou ao implantar",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "Falha ao carregar imagem",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{failed}} falharam de {{count}} verificações",
"feed-asset-action-header": "{{action}} <0>ativo de dados 0>",
"feed-custom-property-header": "propriedades personalizadas atualizadas em",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "Não se preocupe. É hora de reestruturar seus objetivos e progredir mais rapidamente.",
"latency-sla-description": "<0>{{label}}0>: A resposta da consulta deve estar abaixo de <0>{{data}}0>",
"latest-offset-description": "O deslocamento mais recente do evento no sistema.",
+ "learning-resources-management-description": "Explore os recursos do produto e aprenda como eles funcionam através dos nossos recursos",
"leave-the-team-team-name": "Sair da equipe {{teamName}}",
"length-validator-error": "Pelo menos {{length}} {{field}} necessário",
"lineage-ingestion-description": "A ingestão de linhagem pode ser configurada e implantada após uma ingestão de metadados ter sido estabelecida. O fluxo de trabalho de ingestão de linhagem obtém o histórico de consultas, analisa consultas CREATE, INSERT, MERGE... e prepara a linhagem entre as entidades envolvidas. A ingestão de linhagem pode ter apenas um pipeline para um serviço de banco de dados. Defina a Duração do Log de Consulta (em dias) e o Limite de Resultado para iniciar.",
@@ -2512,6 +2536,7 @@
"no-kpi": "Defina indicadores-chave de desempenho para monitorar impacto e tomar decisões mais inteligentes.",
"no-kpi-available-add-new-one": "Nenhum KPI disponível. Clique no botão Adicionar KPI para adicionar um.",
"no-kpi-found": "Nenhum KPI encontrado com o nome {{name}}",
+ "no-learning-resources-available": "Nenhum recurso de aprendizagem disponível para este contexto.",
"no-lineage-available": "Nenhuma conexão de lineage encontrada",
"no-match-found": "Nenhuma correspondência encontrada",
"no-mentions": "Não há instâncias em que você ou sua equipe foram mencionados em atividades",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "Apenas Revisores podem Aprovar ou Rejeitar",
"only-show-columns-with-lineage": "Mostrar apenas colunas com Lineage",
"optional-configuration-update-description-dbt": "Configuração opcional para atualizar a descrição do dbt ou não",
+ "optional-markdown-content": "Opcional: Adicionar conteúdo markdown para incorporar diretamente no recurso",
"owners-coverage-widget-description": "Cobertura de proprietários para todos os ativos de dados no serviço. <0>saiba mais.0>",
"page-is-not-available": "A página que você procura não está disponível",
"page-sub-header-for-activity-feed": "Feed de atividade que permite visualizar um resumo dos eventos de alteração de dados.",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "Descartar",
"unsaved-changes-save": "Salvar alterações",
"unsaved-changes-warning": "Você pode ter alterações não salvas que serão descartadas ao fechar.",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "Solicitar atualização de descrição para",
"update-displayName-entity": "Atualizar o Nome de Exibição para {{entity}}.",
"update-profiler-settings": "Atualizar configurações do examinador.",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "Bem-vindo ao OpenMetadata!",
"welcome-to-open-metadata": "Bem-vindo ao {{brandName}}!",
"would-like-to-start-adding-some": "Gostaria de começar a adicionar alguns?",
+ "write-markdown-content": "Escreva conteúdo markdown aqui...",
"write-your-announcement-lowercase": "escreva seu anúncio",
"write-your-description": "Escreva sua descrição",
"write-your-text": "Escreva o seu {{text}}",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "E-mail verificado com sucesso",
"add-entity-error": "Erro ao adicionar {{entity}}!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "Provedor de autenticação {{provider}} não suportado para renovação de tokens.",
"can-not-renew-token-authentication-not-present": "Não é possível renovar o token de identificação. O provedor de autenticação não está presente.",
"column-fetch-error": "Erro ao buscar caso de teste de coluna!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "Você digitou um nome de usuário ou senha inválido.",
"join-team-error": "Erro ao entrar na equipe!",
"join-team-success": "Equipe ingressada com sucesso!",
+ "learning-resources-fetch-error": "Erro ao buscar recursos de aprendizagem",
"leave-team-error": "Erro ao sair da equipe!",
"leave-team-success": "Saiu da equipe com sucesso!",
"no-application-schema-found": "Nenhum esquema de aplicação encontrado para {{appName}}",
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 0116da6e1bf3..832587494ab6 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
@@ -59,6 +59,7 @@
"add-new-field": "Adicionar Novo Campo",
"add-new-widget-plural": "Adicionar novos widgets",
"add-product": "Adicionar Produto",
+ "add-resource": "Add Resource",
"add-row": "Adicionar Linha",
"add-suggestion": "Adicionar sugestão",
"add-term-boost": "Adicionar Impulso de Termo",
@@ -315,6 +316,8 @@
"container": "Contentor",
"container-column": "Coluna do Contentor",
"container-plural": "Contentores",
+ "content-name": "Content Name",
+ "context": "Contexto",
"contract": "Contrato",
"contract-detail-plural": "Detalhes do Contrato",
"contract-execution-history": "Histórico de execução do contrato",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "Editar Nome de Exibição do Glossário",
"edit-glossary-name": "Editar Nome do Glossário",
"edit-name": "Editar Nome",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "Editar sugestão",
"edit-widget": "Editar widget",
"edit-widget-plural": "Editar Widgets",
@@ -597,6 +601,7 @@
"embed-file-type": "Incorporar {{fileType}}",
"embed-image": "Incorporar imagem",
"embed-link": "Incorporar link",
+ "embedded-content": "Conteúdo Incorporado",
"enable": "Habilitar",
"enable-debug-log": "Habilitar Log de Depuração",
"enable-entity": "Habilitar {{entity}}",
@@ -657,6 +662,7 @@
"entity-reference-plural": "Referências de Entidade",
"entity-reference-types": "Tipos de Referência de Entidade",
"entity-report-data": "Dados do Relatório da Entidade",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}} a Executar",
"entity-scheduled-to-run-value": "{{entity}} agendado para executar {{value}}",
"entity-service": "Serviço de {{entity}}",
@@ -995,8 +1001,11 @@
"latest-offset": "Offset Mais Recente",
"layer": "Camada",
"layer-plural": "Camadas",
+ "learn-how-this-feature-works": "Saiba como esta funcionalidade funciona?",
"learn-more": "Saber Mais",
"learn-more-and-support": "Saiba mais e Suporte",
+ "learning-resource": "Recurso de aprendizagem",
+ "learning-resources": "Recursos de aprendizagem",
"leave-team": "Deixar o Time",
"legend": "Legenda",
"less": "Menos",
@@ -1025,6 +1034,7 @@
"live": "Ao Vivo",
"load-more": "Carregar Mais",
"loading": "Carregando",
+ "loading-article": "Loading article...",
"loading-graph": "A carregar gráfico",
"local-config-source": "Fonte de Configuração Local",
"location": "Localização",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "Tipo Mime",
"min": "Min",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "Mapa Mental",
"minor": "Menor",
"minute": "Minuto",
@@ -1123,6 +1135,7 @@
"month": "Mês",
"monthly": "Mensal",
"more": "Mais",
+ "more-action": "More Action",
"more-help": "Mais Ajuda",
"more-lowercase": "mais",
"most-active-user": "Utilizador Mais Ativo",
@@ -1244,6 +1257,7 @@
"open-metadata": "OpenMetadata",
"open-metadata-logo": "Logo OpenMetadata",
"open-metadata-url": "URL do OpenMetadata",
+ "open-original": "Open Original",
"open-task-plural": "Tarefas Abertas",
"operation": "Operação",
"operation-plural": "Operações",
@@ -1273,6 +1287,7 @@
"ownership": "Propriedade",
"page": "Página",
"page-not-found": "Página Não Encontrada",
+ "page-plural": "Páginas",
"page-views-by-data-asset-plural": "Visualizações de Página por Ativos de Dados",
"parameter": "Parâmetro",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "Selecionar Colunas para Excluir",
"select-column-plural-to-include": "Selecionar Colunas para Incluir",
"select-dimension": "Seleccionar dimensão",
+ "select-duration": "Select Duration",
"select-entity": "Selecionar {{entity}}",
"select-entity-type": "Selecionar tipo de entidade",
"select-field": "Selecionar {{field}}",
"select-field-and-weight": "Selecionar campo e seu peso",
+ "select-page-plural": "Select pages",
"select-schema-object": "Selecionar objeto de esquema",
+ "select-status": "Select Status",
"select-tag": "Selecione uma tag",
"select-test-type": "Selecionar tipo de teste",
"select-to-search": "Pesquisar para Selecionar",
@@ -1675,6 +1693,7 @@
"source-label": "Fonte:",
"source-match": "Correspondência por Fonte de Evento",
"source-plural": "Fontes",
+ "source-provider": "Fornecedor de Origem",
"source-url": "URL de origem",
"source-with-details": "Fonte: {{source}} ({{entityName}})",
"specific-data-asset-plural": "Ativos de Dados Específicos",
@@ -1916,6 +1935,7 @@
"update-image": "Atualizar Imagem",
"update-request-tag-plural": "Atualizar Etiquetas de Solicitação",
"updated": "Atualizado",
+ "updated-at": "Updated at",
"updated-by": "Atualizado por",
"updated-lowercase": "atualizado",
"updated-on": "Atualizado em",
@@ -2260,6 +2280,7 @@
"enter-a-field": "Insira um {{field}}",
"enter-column-description": "Insira a descrição da coluna",
"enter-comma-separated-field": "Insira {{field}} separados por vírgula(,)",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "Insira o nome para exibição",
"enter-feature-description": "Insira a descrição do recurso",
"enter-interval": "Insira o intervalo",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "Apenas destinos externos podem ser testados.",
"failed-events-description": "Contagem de eventos falhados para alerta específico.",
"failed-status-for-entity-deploy": "<0>{{entity}}0> foi {{entityStatus}}, mas falhou ao implantar",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "Falha ao carregar imagem",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{failed}} falharam de {{count}} verificações",
"feed-asset-action-header": "{{action}} <0>ativo de dados0>",
"feed-custom-property-header": "atualizou Propriedades Personalizadas em",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "Aviso: O objetivo do KPI de Descrição ainda não foi atingido, mas ainda há tempo – a sua organização tem {{count}} dias restantes. Para manter o rumo, ative o Relatório de Insights de Dados. Isso permitir-nos-á enviar atualizações semanais a todas as equipas, promovendo a colaboração e o foco no cumprimento dos KPIs da nossa organização.",
"latency-sla-description": "<0>{{label}}0>: A resposta da consulta deve estar abaixo de <0>{{data}}0>",
"latest-offset-description": "O offset mais recente do evento no sistema.",
+ "learning-resources-management-description": "Explore as funcionalidades do produto e aprenda como funcionam através dos nossos recursos",
"leave-the-team-team-name": "Sair da equipa {{teamName}}",
"length-validator-error": "Pelo menos {{length}} {{field}} necessário",
"lineage-ingestion-description": "A ingestão de linhagem pode ser configurada e implantada após uma ingestão de metadados ter sido estabelecida. O fluxo de trabalho de ingestão de linhagem obtém o histórico de consultas, analisa consultas CREATE, INSERT, MERGE... e prepara a linhagem entre as entidades envolvidas. A ingestão de linhagem pode ter apenas um pipeline para um serviço de banco de dados. Defina a Duração do Log de Consulta (em dias) e o Limite de Resultado para iniciar.",
@@ -2512,6 +2536,7 @@
"no-kpi": "Defina indicadores-chave de desempenho para monitorizar impacto e tomar decisões mais inteligentes.",
"no-kpi-available-add-new-one": "Nenhum KPI disponível. Clique no botão Adicionar KPI para adicionar um.",
"no-kpi-found": "Nenhum KPI encontrado com o nome {{name}}",
+ "no-learning-resources-available": "Nenhum recurso de aprendizagem disponível para este contexto.",
"no-lineage-available": "Nenhuma conexão de linhagem encontrada",
"no-match-found": "Nenhuma correspondência encontrada",
"no-mentions": "Parece que você e a sua equipa estão em dia – sem menções em quaisquer atividades por enquanto. Continuem o bom trabalho!",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "Apenas Revisores podem Aprovar ou Rejeitar",
"only-show-columns-with-lineage": "Mostrar apenas colunas com Linhagem",
"optional-configuration-update-description-dbt": "Configuração opcional para atualizar a descrição do dbt ou não",
+ "optional-markdown-content": "Opcional: Adicionar conteúdo markdown para incorporar diretamente no recurso",
"owners-coverage-widget-description": "Cobertura de proprietários para todos os ativos de dados no serviço. <0>saber mais.0>",
"page-is-not-available": "A página que você procura não está disponível",
"page-sub-header-for-activity-feed": "Feed de atividade que permite visualizar um resumo dos eventos de alteração de dados.",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "Descartar",
"unsaved-changes-save": "Guardar alterações",
"unsaved-changes-warning": "Você pode ter alterações não salvas que serão descartadas ao fechar.",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "Solicitar atualização de descrição para",
"update-displayName-entity": "Atualizar o Nome de Exibição para {{entity}}.",
"update-profiler-settings": "Atualizar configurações do examinador.",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "Bem-vindo ao OpenMetadata!",
"welcome-to-open-metadata": "Bem-vindo ao {{brandName}}!",
"would-like-to-start-adding-some": "Gostaria de começar a adicionar alguns?",
+ "write-markdown-content": "Escreva conteúdo markdown aqui...",
"write-your-announcement-lowercase": "escreva seu anúncio",
"write-your-description": "Escreva sua descrição",
"write-your-text": "Escreva o seu {{text}}",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "E-mail verificado com sucesso",
"add-entity-error": "Erro ao adicionar {{entity}}!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "Provedor de autenticação {{provider}} não suportado para renovação de tokens.",
"can-not-renew-token-authentication-not-present": "Não é possível renovar o token de identificação. O provedor de autenticação não está presente.",
"column-fetch-error": "Erro ao buscar caso de teste de coluna!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "Você digitou um nome de utilizador ou senha inválido.",
"join-team-error": "Erro ao entrar na equipa!",
"join-team-success": "Equipa ingressada com sucesso!",
+ "learning-resources-fetch-error": "Erro ao obter recursos de aprendizagem",
"leave-team-error": "Erro ao sair da equipa!",
"leave-team-success": "Saiu da equipa com sucesso!",
"no-application-schema-found": "Nenhum esquema de aplicação encontrado para {{appName}}",
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 712499803216..bd5b67c3b6f6 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
@@ -59,6 +59,7 @@
"add-new-field": "Добавить новое поле",
"add-new-widget-plural": "Добавить новые виджеты",
"add-product": "Добавить продукт",
+ "add-resource": "Add Resource",
"add-row": "Добавить строку",
"add-suggestion": "Добавить предложение",
"add-term-boost": "Добавить вес тега",
@@ -315,6 +316,8 @@
"container": "Контейнер",
"container-column": "Поле контейнера",
"container-plural": "Контейнеры",
+ "content-name": "Content Name",
+ "context": "Контекст",
"contract": "Контракт",
"contract-detail-plural": "Детали контракта",
"contract-execution-history": "История выполнения контракта",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "Изменить отображаемое имя глоссария",
"edit-glossary-name": "Изменить название глоссария",
"edit-name": "Редактировать имя",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "Редактировать предложение",
"edit-widget": "Редактировать виджет",
"edit-widget-plural": "Редактировать виджеты",
@@ -597,6 +601,7 @@
"embed-file-type": "Встроить {{fileType}}",
"embed-image": "Встроить изображение",
"embed-link": "Встроить ссылку",
+ "embedded-content": "Встроенный контент",
"enable": "Разрешить",
"enable-debug-log": "Включить журнал отладки",
"enable-entity": "Включить {{entity}}",
@@ -657,6 +662,7 @@
"entity-reference-plural": "Ссылки на сущности",
"entity-reference-types": "Типы ссылок на сущности",
"entity-report-data": "Данные отчета сущности",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}} выполняется",
"entity-scheduled-to-run-value": "{{entity}} запланировано к выполнению {{value}}",
"entity-service": "Сервис «{{entity}}»",
@@ -995,8 +1001,11 @@
"latest-offset": "Последнее смещение",
"layer": "Слой",
"layer-plural": "Слои",
+ "learn-how-this-feature-works": "Узнайте, как работает эта функция?",
"learn-more": "Узнать больше",
"learn-more-and-support": "Поддержка",
+ "learning-resource": "Учебный ресурс",
+ "learning-resources": "Учебные ресурсы",
"leave-team": "Покинуть команду",
"legend": "Легенда",
"less": "Менее",
@@ -1025,6 +1034,7 @@
"live": "Процесс",
"load-more": "Загрузить больше",
"loading": "Загрузка",
+ "loading-article": "Loading article...",
"loading-graph": "Загрузка графа",
"local-config-source": "Локальный источник конфигурации",
"location": "Расположение",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "MIME-тип",
"min": "Минимум",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "Ментальная карта",
"minor": "Незначительный",
"minute": "Минута",
@@ -1123,6 +1135,7 @@
"month": "Месяц",
"monthly": "Ежемесячно",
"more": "Больше",
+ "more-action": "More Action",
"more-help": "Дополнительная помощь",
"more-lowercase": "больше",
"most-active-user": "Самый активный пользователь",
@@ -1244,6 +1257,7 @@
"open-metadata": "OpenMetadata",
"open-metadata-logo": "Логотип OpenMetadata",
"open-metadata-url": "URL OpenMetadata",
+ "open-original": "Open Original",
"open-task-plural": "Открытые задачи",
"operation": "Операция",
"operation-plural": "Операции",
@@ -1273,6 +1287,7 @@
"ownership": "Владение",
"page": "Страница",
"page-not-found": "Страница не найдена",
+ "page-plural": "Страницы",
"page-views-by-data-asset-plural": "Просмотры объектов данных",
"parameter": "Параметр",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "Выберите столбцы для исключения",
"select-column-plural-to-include": "Выберите столбцы для исключения",
"select-dimension": "Выбрать измерение",
+ "select-duration": "Select Duration",
"select-entity": "Выбрать объект «{{entity}}»",
"select-entity-type": "Выберите тип сущности",
"select-field": "Выбрать {{field}}",
"select-field-and-weight": "Выберите поле и его вес",
+ "select-page-plural": "Выбрать страницы",
"select-schema-object": "Выбрать объект схемы",
+ "select-status": "Select Status",
"select-tag": "Выберите тег",
"select-test-type": "Выберите тип теста",
"select-to-search": "Поиск для выбора",
@@ -1675,6 +1693,7 @@
"source-label": "Источник:",
"source-match": "Сопоставление по источнику события",
"source-plural": "Источники",
+ "source-provider": "Поставщик источника",
"source-url": "URL источника",
"source-with-details": "Источник: {{source}} ({{entityName}})",
"specific-data-asset-plural": "Специфические объекты данных",
@@ -1916,6 +1935,7 @@
"update-image": "Обновить изображение",
"update-request-tag-plural": "Обновить теги",
"updated": "Обновлено",
+ "updated-at": "Updated at",
"updated-by": "Обновлено",
"updated-lowercase": "обновил(а)",
"updated-on": "Обновление",
@@ -2260,6 +2280,7 @@
"enter-a-field": "Введите {{field}}",
"enter-column-description": "Введите описание столбца",
"enter-comma-separated-field": "Введите значения поля «{{field}}» через запятую ",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "Введите отображаемое имя",
"enter-feature-description": "Введите описание функции",
"enter-interval": "Введите интервал",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "Можно тестировать только внешние назначения.",
"failed-events-description": "Количество неудачных событий для конкретного оповещения.",
"failed-status-for-entity-deploy": "Объект «<0>{{entity}}0>» был {{entityStatus}}, но не удалось развернуть",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "Не удалось загрузить изображение",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{failed}} не прошли из {{count}} проверок",
"feed-asset-action-header": "{{action}} <0>объект данных0>",
"feed-custom-property-header": "обновлены дополнительные поля для",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "Не беспокойтесь. Пришло время перестроить свои цели и двигаться быстрее.",
"latency-sla-description": "<0>{{label}}0>: Время ответа на запрос должно быть меньше <0>{{data}}0>.",
"latest-offset-description": "Последнее смещение события в системе.",
+ "learning-resources-management-description": "Изучайте возможности продукта и узнавайте, как они работают, с помощью наших ресурсов",
"leave-the-team-team-name": "Покинуть команду «{{teamName}}»",
"length-validator-error": "Требуется не менее {{length}} {{field}}",
"lineage-ingestion-description": "Прием происхождения можно настроить и развернуть после настройки приема метаданных. Рабочий процесс приема происхождения получает историю запросов, анализирует запросы CREATE, INSERT, MERGE... и подготавливает происхождение между вовлеченными сущности. У приема происхождения может быть только один конвейер для службы базы данных. Определите продолжительность журнала запросов (в днях) и лимит результатов для запуска.",
@@ -2512,6 +2536,7 @@
"no-kpi": "Ключевые показатели эффективности (KPI) пока не установлены! Они помогут поставить цели для улучшения документации, эффективного владения и эффективного многоуровневого управления.",
"no-kpi-available-add-new-one": "Нет доступных KPI. Нажмите кнопку «Добавить KPI», чтобы добавить его.",
"no-kpi-found": "KPI с именем {{name}} не найден",
+ "no-learning-resources-available": "Для этого контекста нет доступных учебных ресурсов.",
"no-lineage-available": "Не найдено связей lineage",
"no-match-found": "Совпадения не найдены",
"no-mentions": "Вас или вашу команду не упоминали в каких-либо действиях.",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "Только согласующие могут подтвердить или отклонить термин",
"only-show-columns-with-lineage": "Показать только столбцы с Lineage",
"optional-configuration-update-description-dbt": "Необязательная конфигурация для обновления описания из dbt или нет",
+ "optional-markdown-content": "Необязательно: Добавить markdown контент для встраивания непосредственно в ресурс",
"owners-coverage-widget-description": "Покрытие владельцами для всех объектов данных в сервисе. <0>узнать больше.0>",
"page-is-not-available": "Страница, которую вы ищете, недоступна",
"page-sub-header-for-activity-feed": "Лента активности показывает изменения в объектах данных.",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "Отменить",
"unsaved-changes-save": "Сохранить изменения",
"unsaved-changes-warning": "У вас могут быть несохраненные изменения, которые будут потеряны при закрытии.",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "Нуждается в описании",
"update-displayName-entity": "Обновите отображаемое имя для объекта «{{entity}}».",
"update-profiler-settings": "Обновить настройки профилировщика.",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "Добро пожаловать в OpenMetadata!",
"welcome-to-open-metadata": "Добро пожаловать в {{brandName}}!",
"would-like-to-start-adding-some": "Хотели бы добавить что-нибудь еще?",
+ "write-markdown-content": "Напишите markdown контент здесь...",
"write-your-announcement-lowercase": "Напишите объявление",
"write-your-description": "Добавьте описание",
"write-your-text": "Напишите {{text}}",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "Электронная почта подтверждена",
"add-entity-error": "Ошибка при добавлении объекта «{{entity}}!»",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "Поставщик аутентификации {{provider}} не поддерживает обновление токенов.",
"can-not-renew-token-authentication-not-present": "Не удается обновить токен идентификатора. Поставщик аутентификации отсутствует.",
"column-fetch-error": "Ошибка при получении тестового примера столбца!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "Неверное имя пользователя или пароль.",
"join-team-error": "Ошибка при вступлении в команду!",
"join-team-success": "Вы присоединились к команде!",
+ "learning-resources-fetch-error": "Ошибка при получении учебных ресурсов",
"leave-team-error": "Ошибка при выходе из команды!",
"leave-team-success": "Вы покинули команду!",
"no-application-schema-found": "Схема приложения не найдена для {{appName}}",
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 a0a6c153bf3f..6e1657938b09 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
@@ -59,6 +59,7 @@
"add-new-field": "เพิ่มฟิลด์ใหม่",
"add-new-widget-plural": "เพิ่มวิดเจ็ตใหม่",
"add-product": "เพิ่มผลิตภัณฑ์",
+ "add-resource": "Add Resource",
"add-row": "เพิ่มแถว",
"add-suggestion": "เพิ่มข้อเสนอแนะ",
"add-term-boost": "เพิ่มการเพิ่มคำศัพท์",
@@ -315,6 +316,8 @@
"container": "คอนเทนเนอร์",
"container-column": "คอลัมน์คอนเทนเนอร์",
"container-plural": "คอนเทนเนอร์",
+ "content-name": "Content Name",
+ "context": "บริบท",
"contract": "Contract",
"contract-detail-plural": "Contract Details",
"contract-execution-history": "ประวัติการดำเนินการสัญญา",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "แก้ไขชื่อแสดงสารานุกรม",
"edit-glossary-name": "แก้ไขชื่อสารานุกรม",
"edit-name": "แก้ไขชื่อ",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "แก้ไขข้อเสนอแนะ",
"edit-widget": "แก้ไขวิดเจ็ต",
"edit-widget-plural": "แก้ไขวิดเจ็ต",
@@ -597,6 +601,7 @@
"embed-file-type": "Embed {{fileType}}",
"embed-image": "ฝังภาพ",
"embed-link": "ฝังลิงก์",
+ "embedded-content": "เนื้อหาฝังตัว",
"enable": "เปิดใช้งาน",
"enable-debug-log": "เปิดใช้งานบันทึกการดีบัก",
"enable-entity": "เปิดใช้งาน {{entity}}",
@@ -657,6 +662,7 @@
"entity-reference-plural": "การอ้างอิงเอนทิตีหลายรายการ",
"entity-reference-types": "ประเภทการอ้างอิงเอนทิตี",
"entity-report-data": "ข้อมูลรายงานเอนทิตี้",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}} กำลังทำงาน",
"entity-scheduled-to-run-value": "{{entity}} ถูกกำหนดให้ทำงาน {{value}}",
"entity-service": "บริการ {{entity}}",
@@ -995,8 +1001,11 @@
"latest-offset": "ออฟเซ็ตล่าสุด",
"layer": "ชั้น",
"layer-plural": "ชั้นหลายรายการ",
+ "learn-how-this-feature-works": "เรียนรู้ว่าฟีเจอร์นี้ทำงานอย่างไร?",
"learn-more": "เรียนรู้เพิ่มเติม",
"learn-more-and-support": "เรียนรู้เพิ่มเติม & สนับสนุน",
+ "learning-resource": "ทรัพยากรการเรียนรู้",
+ "learning-resources": "ทรัพยากรการเรียนรู้",
"leave-team": "ออกจากทีม",
"legend": "Legend",
"less": "น้อยลง",
@@ -1025,6 +1034,7 @@
"live": "สด",
"load-more": "โหลดเพิ่มเติม",
"loading": "กำลังโหลด",
+ "loading-article": "Loading article...",
"loading-graph": "Loading Graph",
"local-config-source": "แหล่งที่มาของการกำหนดค่าท้องถิ่น",
"location": "ตำแหน่ง",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "ประเภท MIME",
"min": "ขั้นต่ำ",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "แผนผังความคิด",
"minor": "รอง",
"minute": "นาที",
@@ -1123,6 +1135,7 @@
"month": "เดือน",
"monthly": "รายเดือน",
"more": "เพิ่มเติม",
+ "more-action": "More Action",
"more-help": "ช่วยเหลือเพิ่มเติม",
"more-lowercase": "เพิ่มเติม",
"most-active-user": "ผู้ใช้งานที่ใช้งานมากที่สุด",
@@ -1244,6 +1257,7 @@
"open-metadata": "OpenMetadata",
"open-metadata-logo": "โลโก้ OpenMetadata",
"open-metadata-url": "URL ของ OpenMetadata",
+ "open-original": "Open Original",
"open-task-plural": "งานที่เปิดอยู่",
"operation": "Operation",
"operation-plural": "การดำเนินการ",
@@ -1273,6 +1287,7 @@
"ownership": "ความเป็นเจ้าของ",
"page": "Page",
"page-not-found": "ไม่พบหน้า",
+ "page-plural": "หน้า",
"page-views-by-data-asset-plural": "จำนวนการเข้าชมหน้าโดยสินทรัพย์ข้อมูล",
"parameter": "พารามิเตอร์",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "เลือกคอลัมน์เพื่อลบ",
"select-column-plural-to-include": "เลือกคอลัมน์เพื่อนำเข้า",
"select-dimension": "เลือกมิติ",
+ "select-duration": "Select Duration",
"select-entity": "เลือก {{entity}}",
"select-entity-type": "เลือกประเภทเอนทิตี",
"select-field": "เลือก {{field}}",
"select-field-and-weight": "เลือกฟิลด์และน้ำหนัก",
+ "select-page-plural": "เลือกหน้า",
"select-schema-object": "เลือกออบเจ็กต์สคีมา",
+ "select-status": "Select Status",
"select-tag": "เลือกแท็ก",
"select-test-type": "เลือกประเภทการทดสอบ",
"select-to-search": "ค้นหาเพื่อเลือก",
@@ -1675,6 +1693,7 @@
"source-label": "Source:",
"source-match": "จับคู่ตามแหล่งที่มาของเหตุการณ์",
"source-plural": "แหล่งที่มาหลายรายการ",
+ "source-provider": "ผู้ให้บริการแหล่งที่มา",
"source-url": "URL แหล่งที่มา",
"source-with-details": "แหล่งที่มา: {{source}} ({{entityName}})",
"specific-data-asset-plural": "สินทรัพย์ข้อมูลเฉพาะ",
@@ -1916,6 +1935,7 @@
"update-image": "อัปเดตภาพ",
"update-request-tag-plural": "อัปเดตแท็กคำขอ",
"updated": "อัปเดตแล้ว",
+ "updated-at": "Updated at",
"updated-by": "อัปเดตโดย",
"updated-lowercase": "อัปเดตแล้ว",
"updated-on": "อัปเดตเมื่อ",
@@ -2260,6 +2280,7 @@
"enter-a-field": "กรอก {{field}}",
"enter-column-description": "กรอกคำอธิบายคอลัมน์",
"enter-comma-separated-field": "กรอก {{field}} ที่แยกด้วยเครื่องหมายจุลภาค(,)",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "กรอกชื่อแสดง",
"enter-feature-description": "กรอกคำอธิบายฟีเจอร์",
"enter-interval": "กรอกช่วงเวลา",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "สามารถทดสอบได้เฉพาะปลายทางภายนอกเท่านั้น",
"failed-events-description": "Count of failed events for specific alert.",
"failed-status-for-entity-deploy": "<0>{{entity}}0> มีสถานะ {{entityStatus}} แต่ล้มเหลวในการเรียกใช้งาน",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "โหลดภาพล้มเหลว",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{failed}} failed out of {{count}} checks",
"feed-asset-action-header": "{{action}} <0>สินทรัพย์ข้อมูล0>",
"feed-custom-property-header": "ปรับปรุงคุณสมบัติที่กำหนดเองเมื่อ",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "หมายเหตุ: เป้าหมาย KPI คำอธิบายยังไม่ได้รับการบรรลุ แต่ยังมีเวลา – องค์กรของคุณมีเวลาที่เหลือ {{count}} วัน เพื่อให้มีเวลาติดตาม กรุณาเปิดใช้รายงานข้อมูลเชิงลึก ซึ่งจะทำให้เราสามารถส่งอัปเดตประจำสัปดาห์ไปยังทีมทั้งหมด เพื่อส่งเสริมการร่วมมือและมุ่งเน้นไปที่การบรรลุ KPIs ขององค์กรของเรา",
"latency-sla-description": "<0>{{label}}0>: เวลาตอบสนองของคำค้นต้องต่ำกว่า <0>{{data}}0>",
"latest-offset-description": "The latest offset of the event in the system.",
+ "learning-resources-management-description": "สำรวจคุณสมบัติของผลิตภัณฑ์และเรียนรู้วิธีการทำงานผ่านทรัพยากรของเรา",
"leave-the-team-team-name": "ออกจากทีม {{teamName}}",
"length-validator-error": "จำเป็นต้องมีอย่างน้อย {{length}} {{field}}",
"lineage-ingestion-description": "การนำเข้าลำดับชั้นสามารถตั้งค่าและเรียกใช้งานได้หลังจากการนำเข้าข้อมูลเมตาถูกตั้งค่าแล้ว กระบวนการนำเข้าลำดับชั้นจะได้รับประวัติการค้นหา, วิเคราะห์คำสั่ง CREATE, INSERT, MERGE... และเตรียมลำดับชั้นระหว่างเอนทิตีที่เกี่ยวข้อง การนำเข้าลำดับชั้นสามารถมีเพียงหนึ่งท่อสำหรับบริการฐานข้อมูล กำหนดระยะเวลาในบันทึกการค้นหา (เป็นวัน) และขีดจำกัดผลลัพธ์เพื่อเริ่มต้น",
@@ -2512,6 +2536,7 @@
"no-kpi": "กำหนดตัวชี้วัดผลงานหลักเพื่อเฝ้าดูผลกระทบและตัดสินใจได้ชาญฉลาดยิ่งขึ้น",
"no-kpi-available-add-new-one": "ไม่มี KPI ที่สามารถใช้งานได้ คลิกที่ปุ่ม เพิ่ม KPI เพื่อลงทะเบียน KPI ใหม่",
"no-kpi-found": "ไม่พบ KPI ด้วยชื่อ {{name}}",
+ "no-learning-resources-available": "ไม่มีทรัพยากรการเรียนรู้สำหรับบริบทนี้",
"no-lineage-available": "ไม่พบการเชื่อมโยง lineage",
"no-match-found": "ไม่พบการจับคู่",
"no-mentions": "ดูเหมือนว่าคุณและทีมของคุณไม่มีการกล่าวถึงในกิจกรรมใด ๆ ตอนนี้ รับมือกับงานของคุณอย่างยอดเยี่ยม!",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "เฉพาะผู้ตรวจสอบเท่านั้นที่สามารถอนุมัติหรือปฏิเสธ",
"only-show-columns-with-lineage": "แสดงเฉพาะคอลัมน์ที่มี Lineage",
"optional-configuration-update-description-dbt": "การตั้งค่าเสริมเพื่ออัปเดตคำอธิบายจาก dbt หรือไม่",
+ "optional-markdown-content": "ตัวเลือก: เพิ่มเนื้อหา markdown เพื่อฝังโดยตรงในทรัพยากร",
"owners-coverage-widget-description": "ความครอบคลุมของเจ้าของสำหรับสินทรัพย์ข้อมูลทั้งหมดในบริการ <0>เรียนรู้เพิ่มเติม0>",
"page-is-not-available": "หน้าเว็บที่คุณกำลังมองหาไม่สามารถใช้งานได้",
"page-sub-header-for-activity-feed": "ฟีดกิจกรรมที่ช่วยให้คุณดูสรุปเหตุการณ์การเปลี่ยนแปลงข้อมูล",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "ยกเลิก",
"unsaved-changes-save": "บันทึกการเปลี่ยนแปลง",
"unsaved-changes-warning": "คุณอาจมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึกซึ่งจะถูกยกเลิกเมื่อปิด",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "คำขอเพื่ออัปเดตคำอธิบายสำหรับ",
"update-displayName-entity": "อัปเดตชื่อแสดงสำหรับ {{entity}}",
"update-profiler-settings": "อัปเดตการตั้งค่าโปรไฟล์เลอร์",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "ยินดีต้อนรับสู่ OpenMetadata!",
"welcome-to-open-metadata": "ยินดีต้อนรับสู่ {{brandName}}!",
"would-like-to-start-adding-some": "คุณต้องการเริ่มเพิ่มบางอย่างหรือไม่?",
+ "write-markdown-content": "เขียนเนื้อหา markdown ที่นี่...",
"write-your-announcement-lowercase": "เขียนประกาศของคุณ",
"write-your-description": "เขียนคำอธิบายของคุณ",
"write-your-text": "เขียน {{text}} ของคุณ",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "ยืนยันอีเมลสำเร็จแล้ว",
"add-entity-error": "เกิดข้อผิดพลาดขณะเพิ่ม {{entity}}!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "ผู้ให้บริการรับรองสิทธิ์ {{provider}} ไม่ได้รับการสนับสนุนสำหรับการต่ออายุโทเค็น",
"can-not-renew-token-authentication-not-present": "ไม่สามารถต่ออายุโทเค็นไอดีได้ ผู้ให้บริการรับรองสิทธิ์ไม่สามารถใช้งานได้",
"column-fetch-error": "เกิดข้อผิดพลาดขณะดึงกรณีทดสอบคอลัมน์!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "คุณได้ป้อนชื่อผู้ใช้หรือรหัสผ่านที่ไม่ถูกต้อง",
"join-team-error": "เกิดข้อผิดพลาดขณะเข้าร่วมทีม!",
"join-team-success": "เข้าร่วมทีมสำเร็จ!",
+ "learning-resources-fetch-error": "เกิดข้อผิดพลาดในการดึงทรัพยากรการเรียนรู้",
"leave-team-error": "เกิดข้อผิดพลาดขณะออกจากทีม!",
"leave-team-success": "ออกจากทีมสำเร็จ!",
"no-application-schema-found": "ไม่พบสคีมาแอปพลิเคชันสำหรับ {{appName}}",
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 64d8e2cbffeb..b1f62c740ada 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
@@ -59,6 +59,7 @@
"add-new-field": "Yeni Alan Ekle",
"add-new-widget-plural": "Yeni widget'lar ekle",
"add-product": "Ürün Ekle",
+ "add-resource": "Add Resource",
"add-row": "Satır Ekle",
"add-suggestion": "Öneri ekle",
"add-term-boost": "Terim Desteği Ekle",
@@ -315,6 +316,8 @@
"container": "Konteyner",
"container-column": "Konteyner Sütunu",
"container-plural": "Konteynerler",
+ "content-name": "Content Name",
+ "context": "Bağlam",
"contract": "Contract",
"contract-detail-plural": "Contract Details",
"contract-execution-history": "Sözleşme Yürütme Geçmişi",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "Sözlük Görünen Adını Düzenle",
"edit-glossary-name": "Sözlük Adını Düzenle",
"edit-name": "Adı Düzenle",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "Öneriyi Düzenle",
"edit-widget": "Widget düzenle",
"edit-widget-plural": "Widget'ları Düzenle",
@@ -597,6 +601,7 @@
"embed-file-type": "{{fileType}} Göm",
"embed-image": "Resim göm",
"embed-link": "Bağlantı göm",
+ "embedded-content": "Gömülü İçerik",
"enable": "Etkinleştir",
"enable-debug-log": "Hata Ayıklama Günlüğünü Etkinleştir",
"enable-entity": "{{entity}} Etkinleştir",
@@ -657,6 +662,7 @@
"entity-reference-plural": "Varlık Referansları",
"entity-reference-types": "Varlık Referans Türleri",
"entity-report-data": "Varlık Rapor Verileri",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}} Çalışıyor",
"entity-scheduled-to-run-value": "{{entity}} {{value}} için çalıştırılacak şekilde planlandı",
"entity-service": "{{entity}} Servisi",
@@ -995,8 +1001,11 @@
"latest-offset": "En Son Ofset",
"layer": "Katman",
"layer-plural": "Katmanlar",
+ "learn-how-this-feature-works": "Bu özelliğin nasıl çalıştığını öğrenin?",
"learn-more": "Daha Fazla Bilgi Edinin",
"learn-more-and-support": "Daha fazla bilgi edinin ve Destek",
+ "learning-resource": "Öğrenme Kaynağı",
+ "learning-resources": "Öğrenme Kaynakları",
"leave-team": "Takımdan Ayrıl",
"legend": "Legend",
"less": "Daha Az",
@@ -1025,6 +1034,7 @@
"live": "Canlı",
"load-more": "Daha Fazla Yükle",
"loading": "Yükleniyor",
+ "loading-article": "Loading article...",
"loading-graph": "Loading Graph",
"local-config-source": "Yerel Yapılandırma Kaynağı",
"location": "Konum",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "MIME Türü",
"min": "Min",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "Zihin Haritası",
"minor": "Küçük",
"minute": "Dakika",
@@ -1123,6 +1135,7 @@
"month": "Ay",
"monthly": "Aylık",
"more": "Daha Fazla",
+ "more-action": "More Action",
"more-help": "Daha Fazla Yardım",
"more-lowercase": "daha fazla",
"most-active-user": "En Aktif Kullanıcı",
@@ -1244,6 +1257,7 @@
"open-metadata": "OpenMetadata",
"open-metadata-logo": "OpenMetadata Logosu",
"open-metadata-url": "OpenMetadata URL'si",
+ "open-original": "Open Original",
"open-task-plural": "Açık Görevler",
"operation": "Operation",
"operation-plural": "İşlemler",
@@ -1273,6 +1287,7 @@
"ownership": "Sahiplik",
"page": "Sayfa",
"page-not-found": "Sayfa Bulunamadı",
+ "page-plural": "Sayfalar",
"page-views-by-data-asset-plural": "Veri Varlıklarına Göre Sayfa Görüntülemeleri",
"parameter": "Parametre",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "Hariç Tutulacak Sütunları Seçin",
"select-column-plural-to-include": "Dahil Edilecek Sütunları Seçin",
"select-dimension": "Boyut Seç",
+ "select-duration": "Select Duration",
"select-entity": "{{entity}} Seç",
"select-entity-type": "Varlık Türü Seçin",
"select-field": "{{field}} Seç",
"select-field-and-weight": "Alan ve ağırlığını seçin",
+ "select-page-plural": "Sayfaları seç",
"select-schema-object": "Şema nesnesi seçin",
+ "select-status": "Select Status",
"select-tag": "Bir etiket seçin",
"select-test-type": "Test Türü Seçin",
"select-to-search": "Seçmek için Ara",
@@ -1675,6 +1693,7 @@
"source-label": "Source:",
"source-match": "Olay Kaynağına Göre Eşleştir",
"source-plural": "Kaynaklar",
+ "source-provider": "Kaynak Sağlayıcı",
"source-url": "Kaynak URL'si",
"source-with-details": "Kaynak: {{source}} ({{entityName}})",
"specific-data-asset-plural": "Belirli Veri Varlıkları",
@@ -1916,6 +1935,7 @@
"update-image": "Resmi Güncelle",
"update-request-tag-plural": "Etiket İsteklerini Güncelle",
"updated": "Güncellendi",
+ "updated-at": "Updated at",
"updated-by": "Güncelleyen",
"updated-lowercase": "güncellendi",
"updated-on": "Güncellenme tarihi",
@@ -2260,6 +2280,7 @@
"enter-a-field": "Bir {{field}} girin",
"enter-column-description": "Sütun açıklaması girin",
"enter-comma-separated-field": "Virgülle ayrılmış {{field}} girin",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "Görünen ad girin",
"enter-feature-description": "Özellik açıklaması girin",
"enter-interval": "Aralık girin",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "Yalnızca harici hedefler test edilebilir.",
"failed-events-description": "Belirli bir uyarı için başarısız olay sayısı.",
"failed-status-for-entity-deploy": "<0>{{entity}}0> {{entityStatus}} oldu, ancak dağıtılamadı",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "Görsel yüklenemedi",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{count}} kontrolden {{failed}} tanesi başarısız",
"feed-asset-action-header": "{{action}} <0>veri varlığı0>",
"feed-custom-property-header": "üzerindeki Özel Özellikler güncellendi",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "Uyarı: Açıklama KPI hedefi henüz karşılanmadı, ancak hala zaman var – kuruluşunuzun {{count}} günü kaldı. Takipte kalmak için lütfen Veri Analizleri Raporunu etkinleştirin. Bu, tüm ekiplere haftalık güncellemeler göndermemizi sağlayacak, böylece kuruluşumuzun KPI'larına ulaşma yönünde işbirliğini ve odaklanmayı teşvik edecektir.",
"latency-sla-description": "<0>{{label}}0>: Sorgu yanıtı <0>{{data}}0>'den az olmalıdır.",
"latest-offset-description": "Sistemdeki olayın en son ofseti.",
+ "learning-resources-management-description": "Ürün özelliklerini keşfedin ve kaynaklarımız aracılığıyla nasıl çalıştıklarını öğrenin",
"leave-the-team-team-name": "{{teamName}} takımından ayrıl",
"length-validator-error": "En az {{length}} {{field}} gerekli",
"lineage-ingestion-description": "Veri soyu alımı, bir metadata alımı ayarlandıktan sonra yapılandırılabilir ve dağıtılabilir. Veri soyu alım iş akışı, sorgu geçmişini alır, CREATE, INSERT, MERGE... sorgularını ayrıştırır ve ilgili varlıklar arasındaki veri soyunu hazırlar. Veri soyu alımının bir veritabanı servisi için yalnızca bir iş akışı olabilir. Başlamak için Sorgu Günlüğü Süresini (gün olarak) ve Sonuç Sınırını tanımlayın.",
@@ -2512,6 +2536,7 @@
"no-kpi": "Etkileri izlemek ve daha akıllı kararlar almak için anahtar performans göstergelerini tanımlayın.",
"no-kpi-available-add-new-one": "Kullanılabilir KPI yok. Bir tane eklemek için KPI Ekle düğmesine tıklayın.",
"no-kpi-found": "{{name}} adında KPI bulunamadı",
+ "no-learning-resources-available": "Bu bağlam için kullanılabilir öğrenme kaynağı yok.",
"no-lineage-available": "Lineage bağlantıları bulunamadı",
"no-match-found": "Eşleşme bulunamadı",
"no-mentions": "Görünüşe göre siz ve ekibiniz tamamen temizsiniz – henüz herhangi bir aktivitede bahsetme yok. Harika çalışmaya devam edin!",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "Yalnızca İnceleyiciler Onaylayabilir veya Reddedebilir",
"only-show-columns-with-lineage": "Yalnızca Lineage olan sütunları göster",
"optional-configuration-update-description-dbt": "Açıklamayı dbt'den güncellemek veya güncellememek için isteğe bağlı yapılandırma",
+ "optional-markdown-content": "İsteğe bağlı: Doğrudan kaynağa gömülecek markdown içeriği ekleyin",
"owners-coverage-widget-description": "Servisteki tüm veri varlıkları için sahip kapsamı. <0>daha fazla bilgi edinin.0>",
"page-is-not-available": "Aradığınız sayfa mevcut değil",
"page-sub-header-for-activity-feed": "Veri değişikliği olaylarının bir özetini görüntülemenizi sağlayan aktivite akışı.",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "At",
"unsaved-changes-save": "Değişiklikleri kaydet",
"unsaved-changes-warning": "Kaydedilmemiş değişiklikleriniz olabilir ve bunlar kapatıldığında kaybolacaktır.",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "için açıklama güncelleme isteği",
"update-displayName-entity": "{{entity}} için Görünen Adı güncelleyin.",
"update-profiler-settings": "Profilleyici ayarını güncelle.",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "OpenMetadata'ya Hoş Geldiniz!",
"welcome-to-open-metadata": "{{brandName}}'ya Hoş Geldiniz!",
"would-like-to-start-adding-some": "Birkaç tane eklemeye başlamak ister misiniz?",
+ "write-markdown-content": "Markdown içeriğini buraya yazın...",
"write-your-announcement-lowercase": "duyurunuzu yazın",
"write-your-description": "Açıklamanızı yazın",
"write-your-text": "{{text}} yazın",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "E-posta başarıyla doğrulandı",
"add-entity-error": "{{entity}} eklenirken hata oluştu!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "Auth Sağlayıcısı {{provider}}, anahtarları yenilemek için desteklenmiyor.",
"can-not-renew-token-authentication-not-present": "Kimlik anahtarı yenilenemiyor. Kimlik Doğrulama Sağlayıcısı mevcut değil.",
"column-fetch-error": "Sütun test senaryosu alınırken hata oluştu!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "Geçersiz bir kullanıcı adı veya şifre girdiniz.",
"join-team-error": "Takıma katılırken hata oluştu!",
"join-team-success": "Takıma başarıyla katıldınız!",
+ "learning-resources-fetch-error": "Öğrenme kaynakları alınırken hata oluştu",
"leave-team-error": "Takımdan ayrılırken hata oluştu!",
"leave-team-success": "Takımdan başarıyla ayrıldınız!",
"no-application-schema-found": "{{appName}} için uygulama şeması bulunamadı",
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 fbe95ab94fe9..52dfd3b7c90c 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
@@ -59,6 +59,7 @@
"add-new-field": "添加新字段",
"add-new-widget-plural": "添加新小部件",
"add-product": "添加产品",
+ "add-resource": "Add Resource",
"add-row": "添加行",
"add-suggestion": "添加建议",
"add-term-boost": "添加术语提升",
@@ -315,6 +316,8 @@
"container": "存储容器",
"container-column": "存储容器列",
"container-plural": "存储容器",
+ "content-name": "Content Name",
+ "context": "上下文",
"contract": "合同",
"contract-detail-plural": "合同详情",
"contract-execution-history": "合同执行历史",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "编辑术语库显示名称",
"edit-glossary-name": "编辑术语库名称",
"edit-name": "编辑名称",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "编辑建议",
"edit-widget": "编辑小组件",
"edit-widget-plural": "编辑小部件",
@@ -597,6 +601,7 @@
"embed-file-type": "Embed {{fileType}}",
"embed-image": "插入图片",
"embed-link": "插入链接",
+ "embedded-content": "嵌入内容",
"enable": "启用",
"enable-debug-log": "启用调试日志",
"enable-entity": "启用{{entity}}",
@@ -657,6 +662,7 @@
"entity-reference-plural": "实体参考",
"entity-reference-types": "实体引用类型",
"entity-report-data": "实体报告数据",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}}运行中",
"entity-scheduled-to-run-value": "{{entity}}计划在{{value}}运行",
"entity-service": "{{entity}} 服务",
@@ -995,8 +1001,11 @@
"latest-offset": "最新偏移量",
"layer": "Layer",
"layer-plural": "面板",
+ "learn-how-this-feature-works": "了解此功能如何运作?",
"learn-more": "Learn More",
"learn-more-and-support": "Learn more & Support",
+ "learning-resource": "学习资源",
+ "learning-resources": "学习资源",
"leave-team": "离开团队",
"legend": "Legend",
"less": "更少",
@@ -1025,6 +1034,7 @@
"live": "实时",
"load-more": "加载更多",
"loading": "加载中",
+ "loading-article": "Loading article...",
"loading-graph": "加载图表",
"local-config-source": "本地配置源",
"location": "位置",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "MIME类型",
"min": "最小",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "思维导图",
"minor": "次要",
"minute": "分钟",
@@ -1123,6 +1135,7 @@
"month": "月",
"monthly": "每月",
"more": "更多",
+ "more-action": "More Action",
"more-help": "更多帮助",
"more-lowercase": "更多",
"most-active-user": "最活跃用户",
@@ -1244,6 +1257,7 @@
"open-metadata": "OpenMetadata",
"open-metadata-logo": "OpenMetadata Logo",
"open-metadata-url": "OpenMetadata URL",
+ "open-original": "Open Original",
"open-task-plural": "打开任务",
"operation": "操作",
"operation-plural": "操作",
@@ -1273,6 +1287,7 @@
"ownership": "所有权",
"page": "页面",
"page-not-found": "没有找到页面",
+ "page-plural": "页面",
"page-views-by-data-asset-plural": "数据资产页面浏览量",
"parameter": "参数",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "选择要排除的列",
"select-column-plural-to-include": "选择要包含的列",
"select-dimension": "选择维度",
+ "select-duration": "Select Duration",
"select-entity": "Select {{entity}}",
"select-entity-type": "选择实体类型",
"select-field": "选择{{field}}",
"select-field-and-weight": "选择字段及其权重",
+ "select-page-plural": "选择页面",
"select-schema-object": "选择架构对象",
+ "select-status": "Select Status",
"select-tag": "选择标签",
"select-test-type": "选择测试类型",
"select-to-search": "搜索以选择",
@@ -1675,6 +1693,7 @@
"source-label": "Source:",
"source-match": "根据事件源匹配",
"source-plural": "来源",
+ "source-provider": "来源提供者",
"source-url": "源 URL",
"source-with-details": "来源: {{source}} ({{entityName}})",
"specific-data-asset-plural": "特定数据资产",
@@ -1916,6 +1935,7 @@
"update-image": "更新图片",
"update-request-tag-plural": "更新请求标签",
"updated": "已更新",
+ "updated-at": "Updated at",
"updated-by": "更新者",
"updated-lowercase": "已更新",
"updated-on": "更新于",
@@ -2260,6 +2280,7 @@
"enter-a-field": "输入{{field}}",
"enter-column-description": "输入列描述",
"enter-comma-separated-field": "输入英文逗号(,)分隔的{{field}}",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "输入显示名称",
"enter-feature-description": "输入功能描述",
"enter-interval": "输入间隔",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "只能测试外部目标。",
"failed-events-description": "特定告警的失败事件计数。",
"failed-status-for-entity-deploy": "<0>{{entity}}0>已{{entityStatus}}, 但无法部署",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "加载图片失败",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{count}} 项检查中 {{failed}} 项失败",
"feed-asset-action-header": "{{action}} <0>data asset0>",
"feed-custom-property-header": "更新了自定义属性于",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "没关系, 现在是重新制定目标并更快进展的时候了。",
"latency-sla-description": "<0>{{label}}0>:查询响应时间必须小于 <0>{{data}}0>。",
"latest-offset-description": "系统中事件的最新偏移量。",
+ "learning-resources-management-description": "探索产品功能,通过我们的资源了解它们的工作原理",
"leave-the-team-team-name": "离开团队{{teamName}}",
"length-validator-error": "至少需要{{length}}个{{field}}",
"lineage-ingestion-description": "在设置元数据提取后, 可以配置并部署血缘提取。血缘提取工作流获取查询历史记录, 解析 CREATE、INSERT、MERGE... 查询, 并整理相关实体之间的血缘。一个数据库服务只能定义一个血缘提取工作流。定义查询日志持续时间(天数)和结果限制即可开始。",
@@ -2512,6 +2536,7 @@
"no-kpi": "定义关键绩效指标,以监控影响并推动更智能的决策。",
"no-kpi-available-add-new-one": "没有可用的 KPI, 请单击添加 KPI 按钮添加一个",
"no-kpi-found": "未找到名称为{{name}}的 KPI",
+ "no-learning-resources-available": "此上下文没有可用的学习资源。",
"no-lineage-available": "未找到 lineage 连接",
"no-match-found": "未找到匹配项",
"no-mentions": "近期您或您的团队没有被任何项目提及",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "只有审核员可以批准或拒绝",
"only-show-columns-with-lineage": "仅显示具有血缘关系的列",
"optional-configuration-update-description-dbt": "可选配置, 选择是否从 dbt 更新描述",
+ "optional-markdown-content": "可选:添加要直接嵌入资源的 markdown 内容",
"owners-coverage-widget-description": "服务中所有数据资产的所有者覆盖率。<0>了解更多。0>",
"page-is-not-available": "您要查找的页面不可用",
"page-sub-header-for-activity-feed": "活动信息流可用于查看数据的修改事件概览",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "丢弃",
"unsaved-changes-save": "保存更改",
"unsaved-changes-warning": "您可能有未保存的更改,关闭时将被��弃。",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "请求更新描述",
"update-displayName-entity": "更改{{entity}}的显示名",
"update-profiler-settings": "更新分析器设置",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "欢迎使用 OpenMetadata!",
"welcome-to-open-metadata": "欢迎使用 {{brandName}}!",
"would-like-to-start-adding-some": "想要从添加一些东西开始吗?",
+ "write-markdown-content": "在此编写 markdown 内容...",
"write-your-announcement-lowercase": "编写您的公告",
"write-your-description": "编写您的描述",
"write-your-text": "请编写您的{{text}}",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "邮箱验证成功",
"add-entity-error": "添加{{entity}}时发生错误!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "{{provider}}不支持续订令牌。",
"can-not-renew-token-authentication-not-present": "无法续订 ID 令牌, 鉴权服务不存在。",
"column-fetch-error": "在获取列测试用例时发生错误!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "您输入的用户名或密码无效。",
"join-team-error": "加入团队时发生错误!",
"join-team-success": "成功加入团队!",
+ "learning-resources-fetch-error": "获取学习资源时出错",
"leave-team-error": "离开团队时发生错误!",
"leave-team-success": "成功离开团队!",
"no-application-schema-found": "未找到{{appName}}的应用程序模式",
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 b7cb5151c926..20c1f8ee9cda 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
@@ -59,6 +59,7 @@
"add-new-field": "新增欄位",
"add-new-widget-plural": "新增小工具",
"add-product": "新增產品",
+ "add-resource": "Add Resource",
"add-row": "新增資料列",
"add-suggestion": "新增建議",
"add-term-boost": "新增詞彙權重",
@@ -315,6 +316,8 @@
"container": "容器",
"container-column": "容器欄位",
"container-plural": "容器",
+ "content-name": "Content Name",
+ "context": "上下文",
"contract": "合約",
"contract-detail-plural": "合約詳情",
"contract-execution-history": "合約執行歷史",
@@ -580,6 +583,7 @@
"edit-glossary-display-name": "編輯詞彙表顯示名稱",
"edit-glossary-name": "編輯詞彙表名稱",
"edit-name": "編輯名稱",
+ "edit-resource": "Edit Resource",
"edit-suggestion": "編輯建議",
"edit-widget": "編輯小工具",
"edit-widget-plural": "編輯小工具",
@@ -597,6 +601,7 @@
"embed-file-type": "嵌入 {{fileType}}",
"embed-image": "嵌入圖片",
"embed-link": "嵌入連結",
+ "embedded-content": "嵌入內容",
"enable": "啟用",
"enable-debug-log": "啟用偵錯日誌",
"enable-entity": "啟用 {{entity}}",
@@ -657,6 +662,7 @@
"entity-reference-plural": "實體參考",
"entity-reference-types": "實體參考類型",
"entity-report-data": "實體報告資料",
+ "entity-resource": "{{entity}}'s Resource",
"entity-running": "{{entity}} 執行中",
"entity-scheduled-to-run-value": "{{entity}} 排程於 {{value}} 執行",
"entity-service": "{{entity}} 服務",
@@ -995,8 +1001,11 @@
"latest-offset": "最新偏移量",
"layer": "層",
"layer-plural": "層",
+ "learn-how-this-feature-works": "了解此功能如何運作?",
"learn-more": "了解更多",
"learn-more-and-support": "了解更多與支援",
+ "learning-resource": "學習資源",
+ "learning-resources": "學習資源",
"leave-team": "離開團隊",
"legend": "Legend",
"less": "較少",
@@ -1025,6 +1034,7 @@
"live": "即時",
"load-more": "載入更多",
"loading": "載入中",
+ "loading-article": "Loading article...",
"loading-graph": "加载图表",
"local-config-source": "本機組態來源",
"location": "位置",
@@ -1099,6 +1109,8 @@
"middot-symbol": "·",
"mime-type": "MIME類型",
"min": "最小值",
+ "min-read": "min read",
+ "min-watch": "min watch",
"mind-map": "心智圖",
"minor": "次要",
"minute": "分鐘",
@@ -1123,6 +1135,7 @@
"month": "月",
"monthly": "每月",
"more": "更多",
+ "more-action": "More Action",
"more-help": "更多說明",
"more-lowercase": "更多",
"most-active-user": "最活躍使用者",
@@ -1244,6 +1257,7 @@
"open-metadata": "OpenMetadata",
"open-metadata-logo": "OpenMetadata 標誌",
"open-metadata-url": "OpenMetadata URL",
+ "open-original": "Open Original",
"open-task-plural": "開啟的任務",
"operation": "操作",
"operation-plural": "操作",
@@ -1273,6 +1287,7 @@
"ownership": "所有權",
"page": "頁面",
"page-not-found": "找不到頁面",
+ "page-plural": "頁面",
"page-views-by-data-asset-plural": "依資料資產的頁面瀏覽量",
"parameter": "參數",
"parameter-description": "Parameter Description",
@@ -1591,11 +1606,14 @@
"select-column-plural-to-exclude": "選取要排除的欄位",
"select-column-plural-to-include": "選取要包含的欄位",
"select-dimension": "選擇維度",
+ "select-duration": "Select Duration",
"select-entity": "選取 {{entity}}",
"select-entity-type": "選取實體類型",
"select-field": "選取 {{field}}",
"select-field-and-weight": "選取欄位及其權重",
+ "select-page-plural": "選擇頁面",
"select-schema-object": "選擇架構對象",
+ "select-status": "Select Status",
"select-tag": "選取標籤",
"select-test-type": "選取測試類型",
"select-to-search": "搜尋以選取",
@@ -1675,6 +1693,7 @@
"source-label": "來源:",
"source-match": "依事件來源比對",
"source-plural": "來源",
+ "source-provider": "來源提供者",
"source-url": "來源 URL",
"source-with-details": "來源:{{source}} ({{entityName}})",
"specific-data-asset-plural": "特定資料資產",
@@ -1916,6 +1935,7 @@
"update-image": "更新圖片",
"update-request-tag-plural": "更新請求標籤",
"updated": "已更新",
+ "updated-at": "Updated at",
"updated-by": "更新者",
"updated-lowercase": "已更新",
"updated-on": "更新於",
@@ -2260,6 +2280,7 @@
"enter-a-field": "輸入一個 {{field}}",
"enter-column-description": "輸入欄位描述",
"enter-comma-separated-field": "輸入以逗號分隔的 {{field}}",
+ "enter-description": "Discover how to automate data governance workflows and task in Collate.",
"enter-display-name": "輸入顯示名稱",
"enter-feature-description": "輸入功能描述",
"enter-interval": "輸入間隔",
@@ -2311,7 +2332,9 @@
"external-destination-selection": "只能測試外部目的地。",
"failed-events-description": "特定警示的失敗事件計數。",
"failed-status-for-entity-deploy": "<0>{{entity}}0> 已 {{entityStatus}},但部署失敗",
+ "failed-to-load-article": "Failed to Load Article",
"failed-to-load-image": "加載圖片失敗",
+ "failed-to-load-learning-resources": "Unable to load learning resources. Please try again later.",
"failed-x-checks": "{{count}} 個檢查中有 {{failed}} 個失敗",
"feed-asset-action-header": "{{action}} <0>資料資產0>",
"feed-custom-property-header": "更新了自訂屬性於",
@@ -2411,6 +2434,7 @@
"kpi-target-overdue": "注意:描述 KPI 目標尚未達成,但還有時間——您的組織還有 {{count}} 天。為保持進度,請啟用資料洞察報告。這將使我們能夠向所有團隊發送每週更新,促進協作並專注於實現我們組織的 KPI。",
"latency-sla-description": "<0>{{label}}0>:查詢回應時間必須低於 <0>{{data}}0>。",
"latest-offset-description": "系統中事件的最新偏移量。",
+ "learning-resources-management-description": "探索產品功能,透過我們的資源了解它們的運作方式",
"leave-the-team-team-name": "離開團隊 {{teamName}}",
"length-validator-error": "至少需要 {{length}} 個 {{field}}",
"lineage-ingestion-description": "在設定元資料擷取後,可以設定和部署血緣擷取。血緣擷取工作流程會取得查詢歷史,剖析 CREATE、INSERT、MERGE... 等查詢,並準備相關實體之間的血緣。血緣擷取對於一個資料庫服務只能有一個管線。定義查詢日誌持續時間(天)和結果限制以開始。",
@@ -2512,6 +2536,7 @@
"no-kpi": "定義關鍵績效指標以監控影響並推動更明智的決策。",
"no-kpi-available-add-new-one": "沒有可用的 KPI。點擊「新增 KPI」按鈕以新增一個。",
"no-kpi-found": "找不到名稱為 {{name}} 的 KPI",
+ "no-learning-resources-available": "此上下文沒有可用的學習資源。",
"no-lineage-available": "未找到 lineage 連接",
"no-match-found": "找不到相符項目",
"no-mentions": "看來您和您的團隊一切順利——目前還沒有在任何活動中被提及。繼續保持!",
@@ -2579,6 +2604,7 @@
"only-reviewers-can-approve-or-reject": "只有審核者可以核准或拒絕",
"only-show-columns-with-lineage": "僅顯示具有血緣關係的欄位",
"optional-configuration-update-description-dbt": "是否從 dbt 更新描述的選用組態",
+ "optional-markdown-content": "可選:添加要直接嵌入資源的 markdown 內容",
"owners-coverage-widget-description": "服務中所有資料資產的擁有者涵蓋範圍。<0>了解更多。0>",
"page-is-not-available": "您正在尋找的頁面不可用",
"page-sub-header-for-activity-feed": "活動摘要,可讓您檢視資料變更事件的摘要。",
@@ -2810,6 +2836,7 @@
"unsaved-changes-discard": "捨棄",
"unsaved-changes-save": "儲存變更",
"unsaved-changes-warning": "您可能有未儲存的變更,關閉時將會被捨棄。",
+ "unsupported-resource-type": "Unsupported resource type",
"update-description-message": "請求更新描述",
"update-displayName-entity": "更新 {{entity}} 的顯示名稱。",
"update-profiler-settings": "更新分析器設定。",
@@ -2845,6 +2872,7 @@
"welcome-to-om": "歡迎來到 OpenMetadata!",
"welcome-to-open-metadata": "歡迎來到 {{brandName}}!",
"would-like-to-start-adding-some": "想開始新增一些嗎?",
+ "write-markdown-content": "在此編寫 markdown 內容...",
"write-your-announcement-lowercase": "撰寫您的公告",
"write-your-description": "撰寫您的描述",
"write-your-text": "撰寫您的 {{text}}",
@@ -2853,6 +2881,7 @@
"server": {
"account-verify-success": "電子郵件驗證成功",
"add-entity-error": "新增 {{entity}} 時發生錯誤!",
+ "article-fetch-error": "Error fetching article content",
"auth-provider-not-supported-renewing": "驗證提供者 {{provider}} 不支援更新權杖。",
"can-not-renew-token-authentication-not-present": "無法更新 ID 權杖。驗證提供者不存在。",
"column-fetch-error": "擷取欄位測試案例時發生錯誤!",
@@ -2897,6 +2926,7 @@
"invalid-username-or-password": "您輸入的使用者名稱或密碼無效。",
"join-team-error": "加入團隊時發生錯誤!",
"join-team-success": "成功加入團隊!",
+ "learning-resources-fetch-error": "取得學習資源時發生錯誤",
"leave-team-error": "離開團隊時發生錯誤!",
"leave-team-success": "成功離開團隊!",
"no-application-schema-found": "找不到 {{appName}} 的應用程式結構",
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/Application/ApplicationPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/Application/ApplicationPage.tsx
index a34a31da3b49..8aa923a08992 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/Application/ApplicationPage.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/Application/ApplicationPage.tsx
@@ -160,6 +160,7 @@ const ApplicationPage = () => {
),
subHeader: t(PAGE_HEADERS.APPLICATION.subHeader),
}}
+ learningPageId="automations"
/>
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityPage.tsx
index 40e0b0a39285..fe2004a5f353 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityPage.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityPage.tsx
@@ -139,6 +139,7 @@ const DataQualityPage = () => {
header: t('label.data-quality'),
subHeader: t('message.page-sub-header-for-data-quality'),
}}
+ learningPageId="dataQuality"
/>
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/IncidentManager/IncidentManagerPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/IncidentManager/IncidentManagerPage.tsx
index b5d1202fa335..68c7c8623daa 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/IncidentManager/IncidentManagerPage.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/IncidentManager/IncidentManagerPage.tsx
@@ -10,10 +10,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Col, Row, Typography } from 'antd';
+import { Col, Row, Space, Typography } from 'antd';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import IncidentManager from '../../components/IncidentManager/IncidentManager.component';
+import { LearningIcon } from '../../components/Learning/LearningIcon/LearningIcon.component';
import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1';
import { PAGE_HEADERS } from '../../constants/PageHeaders.constant';
import incidentManagerClassBase from './IncidentManagerClassBase';
@@ -29,12 +30,15 @@ const IncidentManagerPage = () => {
-
- {t(PAGE_HEADERS.INCIDENT_MANAGER.header)}
-
+
+
+ {t(PAGE_HEADERS.INCIDENT_MANAGER.header)}
+
+
+
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourceForm.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourceForm.component.tsx
new file mode 100644
index 000000000000..a59f7eb85c55
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourceForm.component.tsx
@@ -0,0 +1,464 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { CloseOutlined } from '@ant-design/icons';
+import { Button, Drawer, Form, Input, Select, Space, Typography } from 'antd';
+import { AxiosError } from 'axios';
+import React, { useCallback, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { ReactComponent as ArticalIcon } from '../../assets/svg/artical.svg';
+import { ReactComponent as StoryLaneIcon } from '../../assets/svg/story-lane.svg';
+import { ReactComponent as VideoIcon } from '../../assets/svg/video.svg';
+import RichTextEditor from '../../components/common/RichTextEditor/RichTextEditor';
+import {
+ createLearningResource,
+ CreateLearningResource,
+ LearningResource,
+ updateLearningResource,
+} from '../../rest/learningResourceAPI';
+import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
+import './learning-resource-form.less';
+const { TextArea } = Input;
+const { Text } = Typography;
+
+interface LearningResourceFormProps {
+ open: boolean;
+ resource: LearningResource | null;
+ onClose: () => void;
+}
+
+const RESOURCE_TYPES = [
+ {
+ value: 'Video',
+ label: 'Video',
+ icon: ,
+ },
+ {
+ value: 'Storylane',
+ label: 'Storylane',
+ icon: ,
+ },
+ {
+ value: 'Article',
+ label: 'Article',
+ icon: ,
+ },
+];
+const CATEGORIES = [
+ { value: 'Discovery', label: 'Discovery' },
+ { value: 'DataGovernance', label: 'Governance' },
+ { value: 'DataQuality', label: 'Data Quality' },
+ { value: 'Observability', label: 'Observability' },
+ { value: 'Administration', label: 'Admin' },
+ { value: 'AI', label: 'AI' },
+];
+const STATUSES = ['Draft', 'Active', 'Deprecated'];
+const DURATIONS = [
+ '1 min',
+ '2 mins',
+ '3 mins',
+ '5 mins',
+ '10 mins',
+ '15 mins',
+ '30 mins',
+];
+
+const PAGE_IDS = [
+ // Domains & Data Products
+ { value: 'domain', label: 'Domain' },
+ { value: 'dataProduct', label: 'Data Product' },
+ // Glossaries
+ { value: 'glossary', label: 'Glossary' },
+ { value: 'glossaryTerm', label: 'Glossary Term' },
+ // Classification
+ { value: 'classification', label: 'Classification' },
+ { value: 'tags', label: 'Tags' },
+ // Lineage
+ { value: 'lineage', label: 'Lineage' },
+ // Data Insights
+ { value: 'dataInsights', label: 'Data Insights' },
+ { value: 'dataInsightDashboards', label: 'Data Insight Dashboards' },
+ // Data Quality
+ { value: 'dataQuality', label: 'Data Quality' },
+ { value: 'testSuite', label: 'Test Suite' },
+ { value: 'incidentManager', label: 'Incident Manager' },
+ { value: 'profilerConfiguration', label: 'Profiler Configuration' },
+ // Rules Library
+ { value: 'rulesLibrary', label: 'Rules Library' },
+ // Explore & Discovery
+ { value: 'explore', label: 'Explore' },
+ { value: 'table', label: 'Table' },
+ { value: 'dashboard', label: 'Dashboard' },
+ { value: 'pipeline', label: 'Pipeline' },
+ { value: 'topic', label: 'Topic' },
+ { value: 'container', label: 'Container' },
+ { value: 'mlmodel', label: 'ML Model' },
+ { value: 'storedProcedure', label: 'Stored Procedure' },
+ { value: 'searchIndex', label: 'Search Index' },
+ { value: 'apiEndpoint', label: 'API Endpoint' },
+ { value: 'apiCollection', label: 'API Collection' },
+ { value: 'database', label: 'Database' },
+ { value: 'databaseSchema', label: 'Database Schema' },
+ // Home Page
+ { value: 'homePage', label: 'Home Page' },
+ { value: 'myData', label: 'My Data' },
+ // Workflows & Automations
+ { value: 'workflows', label: 'Workflows' },
+ { value: 'automations', label: 'Automations' },
+ // Knowledge Center
+ { value: 'knowledgeCenter', label: 'Knowledge Center' },
+ // SQL Studio
+ { value: 'sqlStudio', label: 'SQL Studio' },
+ { value: 'queryBuilder', label: 'Query Builder' },
+ // Ask Collate
+ { value: 'askCollate', label: 'Ask Collate' },
+ { value: 'aiAssistant', label: 'AI Assistant' },
+ // Metrics
+ { value: 'metrics', label: 'Metrics' },
+ // Observability
+ { value: 'dataObservability', label: 'Data Observability' },
+ { value: 'pipelineObservability', label: 'Pipeline Observability' },
+ { value: 'alerts', label: 'Alerts' },
+ // Administration
+ { value: 'services', label: 'Services' },
+ { value: 'policies', label: 'Policies' },
+ { value: 'roles', label: 'Roles' },
+ { value: 'teams', label: 'Teams' },
+ { value: 'users', label: 'Users' },
+ { value: 'notificationTemplates', label: 'Notification Templates' },
+ { value: 'ingestionRunners', label: 'Ingestion Runners' },
+ { value: 'usage', label: 'Usage' },
+ { value: 'settings', label: 'Settings' },
+];
+
+export const LearningResourceForm: React.FC = ({
+ open,
+ resource,
+ onClose,
+}) => {
+ const { t } = useTranslation();
+ const [form] = Form.useForm();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [embedContent, setEmbedContent] = useState('');
+ const [resourceType, setResourceType] = useState('Article');
+
+ useEffect(() => {
+ if (resource) {
+ const embedConfig = resource.source.embedConfig as Record<
+ string,
+ unknown
+ >;
+ setEmbedContent((embedConfig?.content as string) || '');
+ setResourceType(resource.resourceType);
+ form.setFieldsValue({
+ name: resource.name,
+ displayName: resource.displayName,
+ description: resource.description,
+ resourceType: resource.resourceType,
+ categories: resource.categories,
+ difficulty: resource.difficulty,
+ sourceUrl: resource.source.url,
+ sourceProvider: resource.source.provider,
+ estimatedDuration: resource.estimatedDuration
+ ? Math.floor(resource.estimatedDuration / 60)
+ : undefined,
+ contexts: resource.contexts?.map((ctx) => ctx.pageId) || [],
+ status: resource.status || 'Active',
+ });
+ } else {
+ form.resetFields();
+ setEmbedContent('');
+ setResourceType('Article');
+ }
+ }, [resource, form]);
+
+ const handleSubmit = useCallback(async () => {
+ try {
+ const values = await form.validateFields();
+ setIsSubmitting(true);
+
+ const contexts = values.contexts.map(
+ (ctx: string | { pageId: string; componentId?: string }) => ({
+ componentId:
+ typeof ctx === 'object' ? ctx.componentId || undefined : undefined,
+ pageId: typeof ctx === 'string' ? ctx : ctx.pageId,
+ })
+ );
+
+ const payload: CreateLearningResource = {
+ categories: values.categories,
+ contexts,
+ description: values.description,
+ difficulty: values.difficulty,
+ displayName: values.displayName,
+ estimatedDuration: values.estimatedDuration
+ ? values.estimatedDuration * 60
+ : undefined,
+ name: values.name,
+ resourceType: values.resourceType,
+ source: {
+ embedConfig:
+ values.resourceType === 'Article' && embedContent
+ ? { content: embedContent }
+ : undefined,
+ provider: values.sourceProvider,
+ url: values.sourceUrl,
+ },
+ status: values.status,
+ };
+
+ if (resource) {
+ await updateLearningResource(payload);
+ showSuccessToast(
+ t('message.entity-updated-successfully', {
+ entity: t('label.learning-resource'),
+ })
+ );
+ } else {
+ await createLearningResource(payload);
+ showSuccessToast(
+ t('server.create-entity-success', {
+ entity: t('label.learning-resource'),
+ })
+ );
+ }
+
+ onClose();
+ } catch (error) {
+ if (error instanceof Error && 'errorFields' in error) {
+ return;
+ }
+ showErrorToast(error as AxiosError);
+ } finally {
+ setIsSubmitting(false);
+ }
+ }, [form, resource, embedContent, t, onClose]);
+
+ const parseDuration = (duration: string): number => {
+ const match = duration.match(/(\d+)/);
+
+ return match ? parseInt(match[1], 10) * 60 : 0;
+ };
+
+ const drawerTitle = (
+
+
+ {resource ? t('label.edit-resource') : t('label.add-resource')}
+
+
+
+ );
+
+ const drawerFooter = (
+
+ {t('label.cancel')}
+
+ {t('label.save')}
+
+
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {RESOURCE_TYPES.map((type) => (
+
+
+ {type.icon}
+ {type.label}
+
+
+ ))}
+
+
+
+
+
+
+
+ {
+ if (!contexts || contexts.length < 1) {
+ return Promise.reject(
+ new Error(
+ t('label.field-required', {
+ field: t('label.page-plural'),
+ })
+ )
+ );
+ }
+ },
+ },
+ ]}>
+
+
+
+
+
+
+
+
+
+
+
+
+ ({
+ label: d,
+ value: parseDuration(d),
+ }))}
+ placeholder={t('label.select-duration')}
+ />
+
+
+
+ ({
+ label: status,
+ value: status,
+ }))}
+ placeholder={t('label.select-status')}
+ />
+
+
+ {resourceType === 'Article' && (
+
+
+ {t('message.optional-markdown-content')}
+
+
+
+ )}
+
+
+ );
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourceForm.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourceForm.test.tsx
new file mode 100644
index 000000000000..80a806bf8538
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourceForm.test.tsx
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { act, fireEvent, render, screen } from '@testing-library/react';
+import { LearningResource } from '../../rest/learningResourceAPI';
+import { LearningResourceForm } from './LearningResourceForm.component';
+
+const mockCreateLearningResource = jest.fn();
+const mockUpdateLearningResource = jest.fn();
+
+jest.mock('../../rest/learningResourceAPI', () => ({
+ createLearningResource: jest
+ .fn()
+ .mockImplementation((...args) => mockCreateLearningResource(...args)),
+ updateLearningResource: jest
+ .fn()
+ .mockImplementation((...args) => mockUpdateLearningResource(...args)),
+}));
+
+jest.mock('../../utils/ToastUtils', () => ({
+ showErrorToast: jest.fn(),
+ showSuccessToast: jest.fn(),
+}));
+
+jest.mock('../../components/common/RichTextEditor/RichTextEditor', () =>
+ jest.fn().mockImplementation(() =>
)
+);
+
+jest.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}));
+
+const mockResource: LearningResource = {
+ id: 'test-id-123',
+ name: 'TestResource',
+ displayName: 'Test Resource',
+ description: 'A test learning resource',
+ resourceType: 'Video',
+ categories: ['Discovery'],
+ difficulty: 'Intro',
+ source: {
+ url: 'https://example.com/video',
+ provider: 'YouTube',
+ },
+ contexts: [{ pageId: 'glossary' }],
+ status: 'Active',
+ fullyQualifiedName: 'TestResource',
+ version: 0.1,
+ updatedAt: Date.now(),
+ updatedBy: 'admin',
+};
+
+const mockProps = {
+ open: true,
+ resource: null,
+ onClose: jest.fn(),
+};
+
+describe('LearningResourceForm', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render the form drawer when open', async () => {
+ await act(async () => {
+ render( );
+ });
+
+ expect(document.querySelector('.drawer-title')).toHaveTextContent(
+ 'label.add-resource'
+ );
+ expect(screen.getByTestId('save-resource')).toBeInTheDocument();
+ });
+
+ it('should show edit title when resource is provided', async () => {
+ await act(async () => {
+ render( );
+ });
+
+ expect(document.querySelector('.drawer-title')).toHaveTextContent(
+ 'label.edit-resource'
+ );
+ });
+
+ it('should populate form fields when editing a resource', async () => {
+ await act(async () => {
+ render( );
+ });
+
+ const nameInput = document.querySelector('#name') as HTMLInputElement;
+
+ expect(nameInput).toHaveValue('TestResource');
+ });
+
+ it('should call onClose when cancel button is clicked', async () => {
+ await act(async () => {
+ render( );
+ });
+
+ const cancelBtn = screen.getByText('label.cancel');
+
+ await act(async () => {
+ fireEvent.click(cancelBtn);
+ });
+
+ expect(mockProps.onClose).toHaveBeenCalled();
+ });
+
+ it('should validate required fields before submission', async () => {
+ await act(async () => {
+ render( );
+ });
+
+ const submitBtn = screen.getByTestId('save-resource');
+
+ await act(async () => {
+ fireEvent.click(submitBtn);
+ });
+
+ // Form validation should prevent API call
+ expect(mockCreateLearningResource).not.toHaveBeenCalled();
+ });
+
+ it('should show save button', async () => {
+ await act(async () => {
+ render( );
+ });
+
+ expect(screen.getByTestId('save-resource')).toHaveTextContent('label.save');
+ });
+
+ it('should render all form fields', async () => {
+ await act(async () => {
+ render( );
+ });
+
+ expect(screen.getByText('label.name')).toBeInTheDocument();
+ expect(screen.getByText('label.display-name')).toBeInTheDocument();
+ expect(screen.getByText('label.description')).toBeInTheDocument();
+ expect(screen.getByText('label.type')).toBeInTheDocument();
+ expect(screen.getByText('label.category-plural')).toBeInTheDocument();
+ expect(screen.getByText('label.page-plural')).toBeInTheDocument();
+ expect(screen.getByText('label.source-url')).toBeInTheDocument();
+ expect(screen.getByText('label.source-provider')).toBeInTheDocument();
+ expect(screen.getByText('label.duration')).toBeInTheDocument();
+ expect(screen.getByText('label.status')).toBeInTheDocument();
+ });
+
+ it('should disable name field when editing', async () => {
+ await act(async () => {
+ render( );
+ });
+
+ const nameInput = document.querySelector('#name') as HTMLInputElement;
+
+ expect(nameInput).toBeDisabled();
+ });
+
+ it('should enable name field when creating new', async () => {
+ await act(async () => {
+ render( );
+ });
+
+ const nameInput = document.querySelector('#name') as HTMLInputElement;
+
+ expect(nameInput).not.toBeDisabled();
+ });
+
+ it('should not render when open is false', async () => {
+ await act(async () => {
+ render( );
+ });
+
+ expect(document.querySelector('.drawer-title')).toBeNull();
+ });
+
+ it('should call onClose when close icon is clicked', async () => {
+ await act(async () => {
+ render( );
+ });
+
+ const closeIcon = document.querySelector('.drawer-close');
+
+ await act(async () => {
+ fireEvent.click(closeIcon as Element);
+ });
+
+ expect(mockProps.onClose).toHaveBeenCalled();
+ });
+
+ it('should show RichTextEditor for Article type', async () => {
+ await act(async () => {
+ render( );
+ });
+
+ // By default, resourceType is 'Article' so RichTextEditor should be shown
+ expect(screen.getByText('label.embedded-content')).toBeInTheDocument();
+ expect(screen.getByTestId('rich-text-editor')).toBeInTheDocument();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.less b/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.less
new file mode 100644
index 000000000000..447fcea69ace
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.less
@@ -0,0 +1,470 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import (reference) '../../styles/variables.less';
+
+.learning-resources-page {
+ padding: @padding-lg;
+
+ .breadcrumb-container {
+ margin-bottom: @margin-md;
+
+ .breadcrumb-item {
+ .link-title {
+ color: #535862;
+ font-size: 14px;
+ font-weight: 400;
+
+ &:hover {
+ color: @primary-color;
+ }
+ }
+
+ .text-grey-muted {
+ font-size: 0;
+ margin: 0 8px;
+
+ &::after {
+ content: '>';
+ font-size: 14px;
+ color: #535862;
+ }
+ }
+
+ &:last-child {
+ .link-title,
+ .inactive-link {
+ color: @primary-color;
+ font-weight: 500;
+ }
+ }
+ }
+ }
+
+ .page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 24px;
+ margin-bottom: @margin-md;
+ border-radius: 12px;
+ background: @white;
+ box-shadow: 0 1px 2px 0 rgba(10, 13, 18, 0.05);
+
+ .page-header-title {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ gap: 4px;
+
+ .page-title {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: #181d27;
+ line-height: 28px;
+ }
+
+ .page-description {
+ margin: 0;
+ font-size: 14px;
+ color: #535862;
+ line-height: 20px;
+ }
+ }
+ }
+
+ .content-card {
+ background-color: @white;
+ border-radius: 12px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+ }
+
+ .filter-bar {
+ display: flex;
+ padding: 16px 24px;
+ align-items: center;
+ gap: 24px;
+ background: @white;
+
+ .search-input {
+ width: 240px;
+ height: 36px;
+ border-radius: 8px;
+ background-color: #fafafa;
+ border-color: #e9eaeb;
+
+ .ant-input {
+ font-size: 16px;
+ background-color: transparent;
+
+ &::placeholder {
+ color: #717680;
+ }
+ }
+
+ .search-icon {
+ color: #717680;
+ }
+ }
+
+ .filter-select {
+ &.ant-select {
+ .ant-select-selector {
+ padding: 0 !important;
+ height: 20px !important;
+ min-height: 20px !important;
+ background: transparent !important;
+ border: none !important;
+ box-shadow: none !important;
+ cursor: pointer;
+ }
+
+ .ant-select-selection-item,
+ .ant-select-selection-placeholder {
+ font-size: 14px;
+ font-weight: @font-medium;
+ color: #414651;
+ padding-right: 18px !important;
+ line-height: 20px;
+ }
+
+ .ant-select-arrow {
+ color: #414651;
+ width: 14px;
+ height: 14px;
+ font-size: 10px;
+ right: 0;
+ top: 50%;
+ margin-top: -7px;
+ inset-inline-end: 0;
+
+ .filter-arrow {
+ font-size: 10px;
+ }
+ }
+
+ .ant-select-clear {
+ background: @white;
+ right: 0;
+ }
+
+ &.ant-select-open,
+ &:hover {
+ .ant-select-selector {
+ box-shadow: none !important;
+ }
+ }
+ }
+ }
+
+ .view-toggle {
+ display: flex;
+ align-items: center;
+ margin-left: auto;
+ border: 1px solid #d5d7da;
+ border-radius: 4px;
+ overflow: hidden;
+
+ .ant-btn {
+ border-radius: 0;
+ border: none;
+ padding: 4px 8px;
+ height: 32px;
+ color: @grey-500;
+
+ &.active {
+ background-color: #eff8ff;
+ color: #1570ef;
+ }
+
+ &:hover:not(.active) {
+ background-color: @grey-50;
+ }
+ }
+ }
+ }
+
+ .ant-table-wrapper {
+ .ant-table {
+ border-radius: 0;
+
+ .ant-table-container {
+ border-radius: 0;
+
+ .ant-table-content {
+ border-radius: 0;
+
+ > table {
+ border-radius: 0;
+ }
+ }
+ }
+ }
+
+ .ant-pagination {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 24px;
+ margin: 0 !important;
+ background: @white;
+ box-shadow: 0px -13px 16px -4px rgba(10, 13, 18, 0.04),
+ 0px -4px 6px -2px rgba(10, 13, 18, 0.03);
+
+ .ant-pagination-prev,
+ .ant-pagination-next {
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ min-width: 36px !important;
+ height: 36px !important;
+ margin: 0 !important;
+ border: 1px solid #d5d7da !important;
+ border-radius: 8px !important;
+ background: @white !important;
+ box-shadow: 0px 1px 2px 0px rgba(10, 13, 18, 0.05) !important;
+
+ button,
+ .ant-pagination-item-link {
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ width: 100% !important;
+ height: 100% !important;
+ border: none !important;
+ background: transparent !important;
+ color: #a4a7ae !important;
+ padding: 0 !important;
+
+ .anticon {
+ font-size: 14px;
+ }
+ }
+
+ &:hover {
+ border-color: @primary-color !important;
+
+ button,
+ .ant-pagination-item-link {
+ color: #414651 !important;
+ }
+ }
+
+ &.ant-pagination-disabled {
+ border-color: #e9eaeb !important;
+
+ button,
+ .ant-pagination-item-link {
+ color: #d5d7da !important;
+ }
+ }
+ }
+
+ .ant-pagination-item {
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ min-width: 40px !important;
+ height: 40px !important;
+ margin: 0 !important;
+ border: none !important;
+ border-radius: 8px !important;
+ background: transparent !important;
+ font-size: 14px !important;
+ font-weight: 500 !important;
+ line-height: 40px !important;
+
+ a {
+ color: #717680 !important;
+ padding: 0 !important;
+ }
+
+ &:hover {
+ background: #fafafa !important;
+ }
+
+ &.ant-pagination-item-active {
+ background: #eff8ff !important;
+ border: none !important;
+
+ a {
+ color: #414651 !important;
+ }
+
+ &:hover {
+ background: #eff8ff !important;
+ }
+ }
+ }
+
+ .ant-pagination-jump-prev,
+ .ant-pagination-jump-next {
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ min-width: 40px !important;
+ height: 40px !important;
+ margin: 0 !important;
+ border: none !important;
+
+ .ant-pagination-item-container {
+ .ant-pagination-item-ellipsis {
+ color: #717680 !important;
+ }
+ }
+ }
+ }
+ }
+
+ .ant-table {
+ .ant-table-thead > tr > th {
+ height: 44px;
+ padding: 12px 24px;
+ background: #fafafa !important;
+ border-bottom: none;
+ font-weight: @font-medium;
+ color: #535862;
+ font-size: 12px;
+
+ &:first-child {
+ padding-left: 24px;
+ }
+
+ &:last-child {
+ padding-right: 24px;
+ }
+ }
+
+ .ant-table-thead > tr {
+ > th:first-child {
+ border-start-start-radius: 0 !important;
+ }
+
+ > th:last-child {
+ border-start-end-radius: 0 !important;
+ }
+ }
+
+ .ant-table-thead {
+ box-shadow: none;
+ }
+
+ .ant-table-tbody > tr > td {
+ padding: 16px 24px;
+ border-bottom: 1px solid #f0f0f0;
+ background-color: @white;
+ vertical-align: middle;
+
+ &:first-child {
+ padding-left: 24px;
+ }
+
+ &:last-child {
+ padding-right: 24px;
+ }
+ }
+
+ .ant-table-tbody > tr:hover > td {
+ background-color: #fafafa;
+ }
+
+ .ant-table-tbody > tr:last-child > td {
+ border-bottom: none;
+ }
+ }
+
+ .content-name-cell {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ cursor: pointer;
+
+ &:hover .content-name {
+ color: @primary-color;
+ }
+
+ .type-icon-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ border-radius: 4px;
+ flex-shrink: 0;
+ font-size: 12px;
+ }
+
+ .content-name {
+ font-size: 14px;
+ color: #181d27;
+ transition: color 0.2s ease;
+ font-weight: 500;
+ }
+ }
+
+ .category-tags,
+ .context-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+
+ .category-tag {
+ margin: 0;
+ font-size: 12px;
+ line-height: 18px;
+ padding: 2px 6px;
+ border-radius: 6px;
+ font-weight: @font-medium;
+ border-width: 1px;
+ border-style: solid;
+ }
+
+ .context-tag {
+ margin: 0;
+ font-size: 12px;
+ line-height: 18px;
+ padding: 2px 6px;
+ border-radius: 6px;
+ font-weight: @font-medium;
+ background-color: #fafafa;
+ border: 1px solid #e9eaeb;
+ color: #414651;
+ }
+
+ .more-tag {
+ background-color: #fafafa;
+ border: 1px solid #e9eaeb;
+ color: #414651;
+ }
+ }
+
+ .action-buttons {
+ .action-btn {
+ color: @grey-500;
+
+ &:hover {
+ color: @grey-700;
+ background-color: @grey-100;
+ }
+
+ &.delete-btn:hover {
+ color: @error-color;
+ background-color: @red-9;
+ }
+ }
+ }
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.tsx
new file mode 100644
index 000000000000..db5b0a47b77d
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.tsx
@@ -0,0 +1,589 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ AppstoreOutlined,
+ DeleteOutlined,
+ DownOutlined,
+ EditOutlined,
+ MenuOutlined,
+ PlusOutlined,
+ SearchOutlined,
+} from '@ant-design/icons';
+import { Button, Input, Modal, Select, Space, Table, Tag, Tooltip } from 'antd';
+import { ColumnsType } from 'antd/lib/table';
+import { AxiosError } from 'axios';
+import { DateTime } from 'luxon';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { ReactComponent as ArticalIcon } from '../../assets/svg/artical.svg';
+import { ReactComponent as StoryLaneIcon } from '../../assets/svg/story-lane.svg';
+import { ReactComponent as VideoIcon } from '../../assets/svg/video.svg';
+import TitleBreadcrumb from '../../components/common/TitleBreadcrumb/TitleBreadcrumb.component';
+import { LEARNING_CATEGORIES } from '../../components/Learning/Learning.interface';
+import { ResourcePlayerModal } from '../../components/Learning/ResourcePlayer/ResourcePlayerModal.component';
+import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1';
+import { GlobalSettingsMenuCategory } from '../../constants/GlobalSettings.constants';
+import {
+ deleteLearningResource,
+ getLearningResourcesList,
+ LearningResource,
+} from '../../rest/learningResourceAPI';
+import { getSettingPath } from '../../utils/RouterUtils';
+import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
+import { LearningResourceForm } from './LearningResourceForm.component';
+import './LearningResourcesPage.less';
+
+const RESOURCE_TYPES = ['Article', 'Video', 'Storylane'];
+const CATEGORIES = [
+ 'Discovery',
+ 'Administration',
+ 'DataGovernance',
+ 'DataQuality',
+ 'Observability',
+ 'AI',
+];
+const CONTENT_CONTEXTS = [
+ { value: 'domain', label: 'Domain' },
+ { value: 'dataProduct', label: 'Data Product' },
+ { value: 'glossary', label: 'Glossary' },
+ { value: 'glossaryTerm', label: 'Glossary Term' },
+ { value: 'classification', label: 'Classification' },
+ { value: 'tags', label: 'Tags' },
+ { value: 'lineage', label: 'Lineage' },
+ { value: 'dataInsights', label: 'Data Insights' },
+ { value: 'dataQuality', label: 'Data Quality' },
+ { value: 'testSuite', label: 'Test Suite' },
+ { value: 'incidentManager', label: 'Incident Manager' },
+ { value: 'explore', label: 'Explore' },
+ { value: 'table', label: 'Table' },
+ { value: 'dashboard', label: 'Dashboard' },
+ { value: 'pipeline', label: 'Pipeline' },
+ { value: 'topic', label: 'Topic' },
+ { value: 'container', label: 'Container' },
+ { value: 'mlmodel', label: 'ML Model' },
+ { value: 'storedProcedure', label: 'Stored Procedure' },
+ { value: 'searchIndex', label: 'Search Index' },
+ { value: 'apiEndpoint', label: 'API Endpoint' },
+ { value: 'database', label: 'Database' },
+ { value: 'databaseSchema', label: 'Database Schema' },
+ { value: 'homePage', label: 'Home Page' },
+ { value: 'workflows', label: 'Workflows' },
+ { value: 'automations', label: 'Automations' },
+ { value: 'knowledgeCenter', label: 'Knowledge Center' },
+ { value: 'sqlStudio', label: 'SQL Studio' },
+ { value: 'askCollate', label: 'Ask Collate' },
+ { value: 'metrics', label: 'Metrics' },
+ { value: 'dataObservability', label: 'Data Observability' },
+ { value: 'pipelineObservability', label: 'Pipeline Observability' },
+ { value: 'alerts', label: 'Alerts' },
+ { value: 'services', label: 'Services' },
+ { value: 'policies', label: 'Policies' },
+ { value: 'roles', label: 'Roles' },
+ { value: 'teams', label: 'Teams' },
+ { value: 'users', label: 'Users' },
+ { value: 'settings', label: 'Settings' },
+];
+const STATUSES = ['Draft', 'Active', 'Deprecated'];
+const MAX_VISIBLE_TAGS = 2;
+const MAX_VISIBLE_CONTEXTS = 3;
+const DEFAULT_PAGE_SIZE = 10;
+
+export const LearningResourcesPage: React.FC = () => {
+ const { t } = useTranslation();
+ const [resources, setResources] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isFormOpen, setIsFormOpen] = useState(false);
+ const [isPlayerOpen, setIsPlayerOpen] = useState(false);
+ const [selectedResource, setSelectedResource] =
+ useState(null);
+ const [editingResource, setEditingResource] =
+ useState(null);
+ const [searchText, setSearchText] = useState('');
+ const [filterType, setFilterType] = useState();
+ const [filterCategory, setFilterCategory] = useState();
+ const [filterContent, setFilterContent] = useState();
+ const [filterStatus, setFilterStatus] = useState();
+ const [viewMode, setViewMode] = useState<'list' | 'card'>('list');
+ const [currentPage, setCurrentPage] = useState(1);
+ const [pageSize] = useState(DEFAULT_PAGE_SIZE);
+
+ const fetchResources = useCallback(async () => {
+ setIsLoading(true);
+ try {
+ const response = await getLearningResourcesList({
+ limit: 100,
+ fields: 'categories,contexts,difficulty,estimatedDuration,owners',
+ });
+ setResources(response.data || []);
+ } catch (error) {
+ showErrorToast(
+ error as AxiosError,
+ t('server.learning-resources-fetch-error')
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ }, [t]);
+
+ useEffect(() => {
+ fetchResources();
+ }, [fetchResources]);
+
+ const filteredResources = useMemo(() => {
+ return resources.filter((resource) => {
+ const matchesSearch =
+ !searchText ||
+ resource.name.toLowerCase().includes(searchText.toLowerCase()) ||
+ resource.displayName?.toLowerCase().includes(searchText.toLowerCase());
+
+ const matchesType = !filterType || resource.resourceType === filterType;
+ const matchesCategory =
+ !filterCategory ||
+ resource.categories?.includes(
+ filterCategory as
+ | 'Discovery'
+ | 'Administration'
+ | 'DataGovernance'
+ | 'DataQuality'
+ | 'Observability'
+ | 'AI'
+ );
+ const matchesContent =
+ !filterContent ||
+ resource.contexts?.some((ctx) => ctx.pageId === filterContent);
+ const matchesStatus =
+ !filterStatus || (resource.status || 'Active') === filterStatus;
+
+ return (
+ matchesSearch &&
+ matchesType &&
+ matchesCategory &&
+ matchesContent &&
+ matchesStatus
+ );
+ });
+ }, [
+ resources,
+ searchText,
+ filterType,
+ filterCategory,
+ filterContent,
+ filterStatus,
+ ]);
+
+ const handleCreate = useCallback(() => {
+ setEditingResource(null);
+ setIsFormOpen(true);
+ }, []);
+
+ const handleEdit = useCallback((resource: LearningResource) => {
+ setEditingResource(resource);
+ setIsFormOpen(true);
+ }, []);
+
+ const handleDelete = useCallback(
+ async (resource: LearningResource) => {
+ Modal.confirm({
+ centered: true,
+ content: t('message.are-you-sure-delete-entity', {
+ entity: resource.displayName || resource.name,
+ }),
+ okText: t('label.delete'),
+ okType: 'danger',
+ title: t('label.delete-entity', {
+ entity: t('label.learning-resource'),
+ }),
+ onOk: async () => {
+ try {
+ await deleteLearningResource(resource.id);
+ showSuccessToast(
+ t('message.entity-deleted-successfully', {
+ entity: t('label.learning-resource'),
+ })
+ );
+ fetchResources();
+ } catch (error) {
+ showErrorToast(error as AxiosError);
+ }
+ },
+ });
+ },
+ [t, fetchResources]
+ );
+
+ const handlePreview = useCallback((resource: LearningResource) => {
+ setSelectedResource(resource);
+ setIsPlayerOpen(true);
+ }, []);
+
+ const handleFormClose = useCallback(() => {
+ setIsFormOpen(false);
+ setEditingResource(null);
+ fetchResources();
+ }, [fetchResources]);
+
+ const handlePlayerClose = useCallback(() => {
+ setIsPlayerOpen(false);
+ setSelectedResource(null);
+ }, []);
+
+ const getResourceTypeIcon = useCallback((type: string) => {
+ switch (type) {
+ case 'Video':
+ return (
+
+
+
+ );
+ case 'Storylane':
+ return (
+
+
+
+ );
+ case 'Article':
+ return (
+
+ );
+ default:
+ return (
+
+ );
+ }
+ }, []);
+
+ const getCategoryColors = useCallback((category: string) => {
+ const categoryInfo =
+ LEARNING_CATEGORIES[category as keyof typeof LEARNING_CATEGORIES];
+
+ return {
+ bgColor: categoryInfo?.bgColor ?? '#f8f9fc',
+ borderColor: categoryInfo?.borderColor ?? '#d5d9eb',
+ color: categoryInfo?.color ?? '#363f72',
+ };
+ }, []);
+
+ const breadcrumbs = useMemo(
+ () => [
+ {
+ name: t('label.setting-plural'),
+ url: getSettingPath(),
+ },
+ {
+ name: t('label.preference-plural'),
+ url: getSettingPath(GlobalSettingsMenuCategory.PREFERENCES),
+ },
+ {
+ name: t('label.learning-resource'),
+ url: '',
+ },
+ ],
+ [t]
+ );
+
+ const columns: ColumnsType = [
+ {
+ dataIndex: 'displayName',
+ key: 'displayName',
+ render: (_, record) => (
+ handlePreview(record)}>
+ {getResourceTypeIcon(record.resourceType)}
+
+ {record.displayName || record.name}
+
+
+ ),
+ title: t('label.content-name'),
+ width: 300,
+ },
+ {
+ dataIndex: 'categories',
+ key: 'categories',
+ render: (categories: string[]) => {
+ if (!categories || categories.length === 0) {
+ return null;
+ }
+ const visibleCategories = categories.slice(0, MAX_VISIBLE_TAGS);
+ const remaining = categories.length - MAX_VISIBLE_TAGS;
+
+ return (
+
+ {visibleCategories.map((cat) => {
+ const colors = getCategoryColors(cat);
+
+ return (
+
+ {LEARNING_CATEGORIES[cat as keyof typeof LEARNING_CATEGORIES]
+ ?.label ?? cat}
+
+ );
+ })}
+ {remaining > 0 && (
+ +{remaining}
+ )}
+
+ );
+ },
+ title: t('label.category-plural'),
+ width: 250,
+ },
+ {
+ dataIndex: 'contexts',
+ key: 'contexts',
+ render: (contexts: Array<{ pageId: string; componentId?: string }>) => {
+ if (!contexts || contexts.length === 0) {
+ return null;
+ }
+ const visibleContexts = contexts.slice(0, MAX_VISIBLE_CONTEXTS);
+ const remaining = contexts.length - MAX_VISIBLE_CONTEXTS;
+
+ const getContextLabel = (pageId: string) => {
+ const context = CONTENT_CONTEXTS.find((c) => c.value === pageId);
+
+ return context?.label ?? pageId;
+ };
+
+ return (
+
+ {visibleContexts.map((ctx, idx) => (
+
+ {getContextLabel(ctx.pageId)}
+
+ ))}
+ {remaining > 0 && (
+ +{remaining}
+ )}
+
+ );
+ },
+ title: t('label.context'),
+ width: 200,
+ },
+ {
+ dataIndex: 'updatedAt',
+ key: 'updatedAt',
+ render: (updatedAt: number) => {
+ if (!updatedAt) {
+ return '-';
+ }
+
+ return DateTime.fromMillis(updatedAt).toFormat('LLL d, yyyy');
+ },
+ title: t('label.updated-at'),
+ width: 120,
+ },
+ {
+ fixed: 'right',
+ key: 'actions',
+ render: (_, record) => (
+
+
+ }
+ size="small"
+ type="text"
+ onClick={(e) => {
+ e.stopPropagation();
+ handleEdit(record);
+ }}
+ />
+
+
+ }
+ size="small"
+ type="text"
+ onClick={(e) => {
+ e.stopPropagation();
+ handleDelete(record);
+ }}
+ />
+
+
+ ),
+ title: t('label.more-action'),
+ width: 100,
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+ {t('label.learning-resource')}
+
+
+ {t('message.learning-resources-management-description')}
+
+
+
}
+ type="primary"
+ onClick={handleCreate}>
+ {t('label.add-entity', { entity: t('label.resource') })}
+
+
+
+
+
+
}
+ value={searchText}
+ onChange={(e) => setSearchText(e.target.value)}
+ />
+
+
({
+ label: type,
+ value: type,
+ }))}
+ placeholder={t('label.type')}
+ popupMatchSelectWidth={false}
+ suffixIcon={ }
+ value={filterType}
+ variant="borderless"
+ onChange={setFilterType}
+ />
+
+ ({
+ label:
+ LEARNING_CATEGORIES[cat as keyof typeof LEARNING_CATEGORIES]
+ ?.label ?? cat,
+ value: cat,
+ }))}
+ placeholder={t('label.category')}
+ popupMatchSelectWidth={false}
+ suffixIcon={ }
+ value={filterCategory}
+ variant="borderless"
+ onChange={setFilterCategory}
+ />
+
+ }
+ value={filterContent}
+ variant="borderless"
+ onChange={setFilterContent}
+ />
+
+ ({
+ label: status,
+ value: status,
+ }))}
+ placeholder={t('label.status')}
+ popupMatchSelectWidth={false}
+ suffixIcon={ }
+ value={filterStatus}
+ variant="borderless"
+ onChange={setFilterStatus}
+ />
+
+
+ }
+ type="text"
+ onClick={() => setViewMode('list')}
+ />
+ }
+ type="text"
+ onClick={() => setViewMode('card')}
+ />
+
+
+
+
{
+ setCurrentPage(page);
+ },
+ }}
+ rowKey="id"
+ scroll={{ x: 1000 }}
+ />
+
+
+ {isFormOpen && (
+
+ )}
+
+ {selectedResource && (
+
+ )}
+
+
+ );
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/learning-resource-form.less b/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/learning-resource-form.less
new file mode 100644
index 000000000000..40f962df042b
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/learning-resource-form.less
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import (reference) '../../styles/variables.less';
+
+.learning-resource-form-drawer {
+ .ant-drawer-header {
+ padding: @padding-md @padding-lg;
+ border-bottom: 1px solid @border-color-1;
+ }
+
+ .ant-drawer-header-title {
+ flex: 1;
+ }
+
+ .drawer-title-container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+
+ .drawer-title {
+ font-size: 16px;
+ font-weight: @font-semibold;
+ color: @grey-900;
+ }
+
+ .drawer-close {
+ font-size: 14px;
+ color: @grey-500;
+ cursor: pointer;
+ padding: @padding-xs;
+ border-radius: @border-rad-base;
+ transition: all 0.2s ease;
+
+ &:hover {
+ color: @grey-700;
+ background-color: @grey-100;
+ }
+ }
+ }
+
+ .ant-drawer-body {
+ padding: @padding-lg;
+ background-color: @white;
+ }
+
+ .ant-drawer-footer {
+ border-top: 1px solid @border-color-1;
+ padding: @padding-md @padding-lg;
+ }
+
+ .drawer-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: @size-sm;
+ }
+}
+
+.learning-resource-form {
+ .ant-form-item {
+ margin-bottom: @size-mlg;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .ant-form-item-label {
+ padding-bottom: @padding-xs;
+
+ > label {
+ font-size: 13px;
+ font-weight: @font-medium;
+ color: @grey-700;
+ }
+ }
+
+ .form-item-required {
+ .ant-form-item-label {
+ > label::after {
+ content: '*';
+ color: @error-color;
+ margin-left: 2px;
+ }
+ }
+ }
+
+ .ant-input,
+ .ant-select-selector {
+ border-radius: @border-rad-xs;
+ }
+
+ .ant-select-selection-item,
+ .ant-select-item-option-content {
+ .ant-space {
+ display: flex;
+ align-items: center;
+
+ .ant-space-item {
+ display: flex;
+ align-items: center;
+ }
+ }
+ }
+
+ .embedded-content-hint {
+ display: block;
+ margin-bottom: @margin-xs;
+ font-size: 12px;
+ }
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MarketPlacePage/MarketPlacePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MarketPlacePage/MarketPlacePage.tsx
index 76e8c31cd9fd..e5ecbdacf74c 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/MarketPlacePage/MarketPlacePage.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/MarketPlacePage/MarketPlacePage.tsx
@@ -144,6 +144,7 @@ const MarketPlacePage = () => {
),
subHeader: t(PAGE_HEADERS.APPLICATION.subHeader),
}}
+ learningPageId="automations"
/>
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ObservabilityAlertsPage/ObservabilityAlertsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ObservabilityAlertsPage/ObservabilityAlertsPage.tsx
index b80adca793e1..fc9cd08e0d97 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/ObservabilityAlertsPage/ObservabilityAlertsPage.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/ObservabilityAlertsPage/ObservabilityAlertsPage.tsx
@@ -295,7 +295,10 @@ const ObservabilityAlertsPage = () => {
-
+
{(alertResourcePermission?.Create ||
alertResourcePermission?.All) && (
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/PlatformLineage/PlatformLineage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/PlatformLineage/PlatformLineage.tsx
index 9e6d254c5551..c2f2ff4a2a64 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/PlatformLineage/PlatformLineage.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/PlatformLineage/PlatformLineage.tsx
@@ -343,6 +343,7 @@ const PlatformLineage = () => {
}),
subHeader: t(PAGE_HEADERS.PLATFORM_LINEAGE.subHeader),
}}
+ learningPageId="lineage"
/>
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesListPage/PoliciesListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesListPage/PoliciesListPage.tsx
index acbed0001ed8..6007b30c47f3 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesListPage/PoliciesListPage.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesListPage/PoliciesListPage.tsx
@@ -285,6 +285,7 @@ const PoliciesListPage = () => {
header: t(PAGE_HEADERS.POLICIES.header),
subHeader: t(PAGE_HEADERS.POLICIES.subHeader),
}}
+ learningPageId="policies"
/>
{addPolicyPermission && (
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ProfilerConfigurationPage/ProfilerConfigurationPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ProfilerConfigurationPage/ProfilerConfigurationPage.tsx
index 83f71ff99e49..c6ac1f3cc1b5 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/ProfilerConfigurationPage/ProfilerConfigurationPage.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/ProfilerConfigurationPage/ProfilerConfigurationPage.tsx
@@ -168,6 +168,7 @@ const ProfilerConfigurationPage = () => {
'message.page-sub-header-for-profiler-configuration'
),
}}
+ learningPageId="profilerConfiguration"
/>
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesListPage/RolesListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesListPage/RolesListPage.tsx
index 1819fc3cc5fd..336f111fcdf7 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesListPage/RolesListPage.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesListPage/RolesListPage.tsx
@@ -283,6 +283,7 @@ const RolesListPage = () => {
header: t(PAGE_HEADERS.ROLES.header),
subHeader: t(PAGE_HEADERS.ROLES.subHeader),
}}
+ learningPageId="roles"
/>
{addRolePermission && (
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TestSuiteDetailsPage/TestSuiteDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TestSuiteDetailsPage/TestSuiteDetailsPage.component.tsx
index 7eb095c4102e..620c64c9ce8c 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/TestSuiteDetailsPage/TestSuiteDetailsPage.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/TestSuiteDetailsPage/TestSuiteDetailsPage.component.tsx
@@ -37,6 +37,7 @@ import { AddTestCaseList } from '../../components/DataQuality/AddTestCaseList/Ad
import TestSuitePipelineTab from '../../components/DataQuality/TestSuite/TestSuitePipelineTab/TestSuitePipelineTab.component';
import { useEntityExportModalProvider } from '../../components/Entity/EntityExportModalProvider/EntityExportModalProvider.component';
import EntityHeaderTitle from '../../components/Entity/EntityHeaderTitle/EntityHeaderTitle.component';
+import { LearningIcon } from '../../components/Learning/LearningIcon/LearningIcon.component';
import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface';
import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1';
import { INITIAL_PAGING_VALUE } from '../../constants/constants';
@@ -525,6 +526,7 @@ const TestSuiteDetailsPage = () => {
icon={ }
name={testSuite?.name ?? ''}
serviceName="testSuite"
+ suffix={ }
/>
diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/learningResourceAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/learningResourceAPI.ts
new file mode 100644
index 000000000000..f4dd97ac5fac
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/rest/learningResourceAPI.ts
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AxiosResponse } from 'axios';
+import { Operation } from 'fast-json-patch';
+import { PagingResponse } from 'Models';
+import { ListParams } from '../interface/API.interface';
+import { getEncodedFqn } from '../utils/StringsUtils';
+import APIClient from './index';
+
+export interface LearningResource {
+ id: string;
+ name: string;
+ fullyQualifiedName?: string;
+ displayName?: string;
+ description?: string;
+ resourceType: 'Storylane' | 'Video' | 'Article';
+ categories: Array<
+ | 'Discovery'
+ | 'Administration'
+ | 'DataGovernance'
+ | 'DataQuality'
+ | 'Observability'
+ >;
+ difficulty?: 'Intro' | 'Intermediate' | 'Advanced';
+ source: {
+ provider?: string;
+ url: string;
+ embedConfig?: Record;
+ };
+ estimatedDuration?: number;
+ contexts: Array<{
+ pageId: string;
+ componentId?: string;
+ priority?: number;
+ }>;
+ status?: 'Draft' | 'Active' | 'Deprecated';
+ tags?: unknown[];
+ owners?: unknown[];
+ reviewers?: unknown[];
+ followers?: unknown[];
+ version?: number;
+ updatedAt?: number;
+ updatedBy?: string;
+ href?: string;
+ deleted?: boolean;
+}
+
+export interface CreateLearningResource {
+ name: string;
+ displayName?: string;
+ description?: string;
+ resourceType: 'Storylane' | 'Video' | 'Article';
+ categories: Array<
+ | 'Discovery'
+ | 'Administration'
+ | 'DataGovernance'
+ | 'DataQuality'
+ | 'Observability'
+ >;
+ difficulty?: 'Intro' | 'Intermediate' | 'Advanced';
+ source: {
+ provider?: string;
+ url: string;
+ embedConfig?: Record;
+ };
+ estimatedDuration?: number;
+ contexts: Array<{
+ pageId: string;
+ componentId?: string;
+ priority?: number;
+ }>;
+ status?: 'Draft' | 'Active' | 'Deprecated';
+ owners?: unknown[];
+ reviewers?: unknown[];
+ tags?: unknown[];
+}
+
+export type ListLearningResourcesParams = ListParams & {
+ pageId?: string;
+ componentId?: string;
+ category?: string;
+ difficulty?: string;
+ status?: string;
+};
+
+const BASE_URL = '/learning/resources';
+
+export const getLearningResourcesList = async (
+ params?: ListLearningResourcesParams
+) => {
+ const response = await APIClient.get>(
+ BASE_URL,
+ { params }
+ );
+
+ return response.data;
+};
+
+export const getLearningResourceById = async (
+ id: string,
+ params?: ListParams
+) => {
+ const response = await APIClient.get(`${BASE_URL}/${id}`, {
+ params,
+ });
+
+ return response.data;
+};
+
+export const getLearningResourceByName = async (
+ fqn: string,
+ params?: ListParams
+) => {
+ const response = await APIClient.get(
+ `${BASE_URL}/name/${getEncodedFqn(fqn)}`,
+ { params }
+ );
+
+ return response.data;
+};
+
+export const createLearningResource = async (data: CreateLearningResource) => {
+ const response = await APIClient.post<
+ CreateLearningResource,
+ AxiosResponse
+ >(BASE_URL, data);
+
+ return response.data;
+};
+
+export const updateLearningResource = async (data: CreateLearningResource) => {
+ // PUT without ID does create-or-update by name
+ const response = await APIClient.put<
+ CreateLearningResource,
+ AxiosResponse
+ >(BASE_URL, data);
+
+ return response.data;
+};
+
+export const patchLearningResource = async (id: string, patch: Operation[]) => {
+ const response = await APIClient.patch<
+ Operation[],
+ AxiosResponse
+ >(`${BASE_URL}/${id}`, patch);
+
+ return response.data;
+};
+
+export const deleteLearningResource = async (
+ id: string,
+ hardDelete = false
+) => {
+ const response = await APIClient.delete(
+ `${BASE_URL}/${id}`,
+ {
+ params: { hardDelete },
+ }
+ );
+
+ return response.data;
+};
+
+export const restoreLearningResource = async (id: string) => {
+ const response = await APIClient.put>(
+ `${BASE_URL}/restore`,
+ {
+ id,
+ }
+ );
+
+ return response.data;
+};
+
+export const getLearningResourcesByContext = async (
+ pageId: string,
+ params?: ListParams
+) => {
+ return getLearningResourcesList({
+ ...params,
+ pageId,
+ status: 'Active',
+ });
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsClassBase.ts
index cd5151df05ab..5250359e0172 100644
--- a/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsClassBase.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsClassBase.ts
@@ -31,6 +31,7 @@ import { ReactComponent as EmailIcon } from '../assets/svg/email-colored.svg';
import { ReactComponent as FileIcon } from '../assets/svg/file-colored-new.svg';
import { ReactComponent as GlossaryIcon } from '../assets/svg/glossary-term-colored-new.svg';
import { ReactComponent as HealthIcon } from '../assets/svg/health-check.svg';
+import { ReactComponent as LearningIcon } from '../assets/svg/learning-colored.svg';
import { ReactComponent as LineageIcon } from '../assets/svg/lineage-colored.svg';
import { ReactComponent as LoginIcon } from '../assets/svg/login-colored.svg';
import { ReactComponent as MessagingIcon } from '../assets/svg/messaging-colored-new.svg';
@@ -652,6 +653,13 @@ class GlobalSettingsClassBase {
icon: DataAssetRulesIcon,
isBeta: true,
},
+ {
+ label: t('label.learning-resources'),
+ description: t('message.learning-resources-management-description'),
+ isProtected: Boolean(isAdminUser),
+ key: `${GlobalSettingsMenuCategory.PREFERENCES}.${GlobalSettingOptions.LEARNING_RESOURCES}`,
+ icon: LearningIcon,
+ },
],
},
{
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SearchUtils.tsx
index ce41e13955fe..4d42edecae73 100644
--- a/openmetadata-ui/src/main/resources/ui/src/utils/SearchUtils.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchUtils.tsx
@@ -476,4 +476,3 @@ export const getTermQuery = (
},
};
};
-