From baee931b85f27739183ac6645a2e82a32990e535 Mon Sep 17 00:00:00 2001 From: Imri Paran Date: Mon, 26 May 2025 12:02:14 +0200 Subject: [PATCH] MINOR: App changes for collate support (#21068) * feat: prep for collate support - Added support for delayed background jobs. This is to delete the support user after a while. - added ScheduleType.OnlyManual for applications to support only manual triggers - Added a four hour token expiration - Allow a null trigger type in job data (not sure what it's for) * added test for delayed job * added type checking for enumCleanupArgs in navbar * - added a support toten type - added job handler for delete token * fixed whitelist links * patch OnlyManual apps * Update openmetadata-spec/src/main/resources/json/schema/entity/applications/app.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * added indexes for runAt * use NO_JOB_SLEEP_SECONDS in background job test * Fixed NavBar test * removed backticls * fix indexes * change for support app * fix tests --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Karan Hotchandani <33024356+karanh37@users.noreply.github.com> Co-authored-by: karanh37 Co-authored-by: Pere Miquel Brull --- .../native/1.8.0/mysql/schemaChanges.sql | 4 ++ .../native/1.8.0/postgres/schemaChanges.sql | 4 ++ .../service/OpenMetadataApplication.java | 10 ++- .../apps/AbstractNativeApplication.java | 14 ++-- .../service/apps/scheduler/AppScheduler.java | 7 +- .../service/jdbi3/CollectionDAO.java | 2 + .../service/jobs/GenericBackgroundWorker.java | 2 +- .../org/openmetadata/service/jobs/JobDAO.java | 25 +++++-- .../service/jobs/JobHandlerRegistry.java | 9 ++- .../service/resources/apps/AppResource.java | 6 +- .../service/security/JwtFilter.java | 3 +- .../jobs/BackgroundJobWorkerTest.java | 66 +++++++++++++++++-- .../schema/auth/emailVerificationToken.json | 3 +- .../json/schema/auth/supportToken.json | 42 ++++++++++++ .../json/schema/entity/applications/app.json | 44 +++++++++---- .../json/schema/jobs/backgroundJob.json | 10 ++- .../src/main/resources/ui/jest.config.js | 23 ++++--- .../AppContainer/AppContainer.test.tsx | 4 -- .../components/AppContainer/AppContainer.tsx | 4 +- .../ui/src/components/AppRouter/AppRouter.tsx | 4 +- .../AppRouter/UnAuthenticatedAppRouter.tsx | 4 +- .../Auth/AuthProviders/AuthProvider.tsx | 6 +- .../ui/src/components/NavBar/NavBar.tsx | 27 ++++++-- .../AppSchedule/AppSchedule.component.tsx | 8 +-- .../generated/auth/emailVerificationToken.ts | 1 + .../ui/src/generated/auth/supporToken.ts | 54 +++++++++++++++ .../src/generated/entity/applications/app.ts | 1 + .../ui/src/generated/jobs/backgroundJob.ts | 14 +++- .../src/utils/ApplicationRoutesClassBase.ts | 26 ++++++++ .../ui/src/utils/AuthProvider.util.ts | 20 ------ .../ui/src/utils/i18next/i18nextUtil.ts | 2 +- 31 files changed, 356 insertions(+), 93 deletions(-) create mode 100644 bootstrap/sql/migrations/native/1.8.0/mysql/schemaChanges.sql create mode 100644 bootstrap/sql/migrations/native/1.8.0/postgres/schemaChanges.sql create mode 100644 openmetadata-spec/src/main/resources/json/schema/auth/supportToken.json create mode 100644 openmetadata-ui/src/main/resources/ui/src/generated/auth/supporToken.ts diff --git a/bootstrap/sql/migrations/native/1.8.0/mysql/schemaChanges.sql b/bootstrap/sql/migrations/native/1.8.0/mysql/schemaChanges.sql new file mode 100644 index 00000000000..8b7d6e508bc --- /dev/null +++ b/bootstrap/sql/migrations/native/1.8.0/mysql/schemaChanges.sql @@ -0,0 +1,4 @@ +ALTER TABLE background_jobs +ADD COLUMN runAt BIGINT; + +CREATE INDEX background_jobs_run_at_index ON background_jobs(runAt); diff --git a/bootstrap/sql/migrations/native/1.8.0/postgres/schemaChanges.sql b/bootstrap/sql/migrations/native/1.8.0/postgres/schemaChanges.sql new file mode 100644 index 00000000000..8b7d6e508bc --- /dev/null +++ b/bootstrap/sql/migrations/native/1.8.0/postgres/schemaChanges.sql @@ -0,0 +1,4 @@ +ALTER TABLE background_jobs +ADD COLUMN runAt BIGINT; + +CREATE INDEX background_jobs_run_at_index ON background_jobs(runAt); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java index e807d654c60..bdd1933fafb 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java @@ -60,6 +60,7 @@ import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.server.ServerProperties; import org.jdbi.v3.core.Jdbi; import org.jdbi.v3.sqlobject.SqlObjects; +import org.jetbrains.annotations.NotNull; import org.openmetadata.schema.api.security.AuthenticationConfiguration; import org.openmetadata.schema.api.security.AuthorizerConfiguration; import org.openmetadata.schema.api.security.ClientType; @@ -241,8 +242,7 @@ public class OpenMetadataApplication extends Application config) { // Validate Native Application - if (app.getScheduleType().equals(ScheduleType.ScheduledOrManual)) { + if (Set.of(ScheduleType.ScheduledOrManual, ScheduleType.OnlyManual) + .contains(app.getScheduleType())) { AppRuntime runtime = getAppRuntime(app); validateServerExecutableApp(runtime); // Trigger the application with the provided configuration payload diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/apps/scheduler/AppScheduler.java b/openmetadata-service/src/main/java/org/openmetadata/service/apps/scheduler/AppScheduler.java index 8bc4eb56177..5e1f9f35be9 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/apps/scheduler/AppScheduler.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/apps/scheduler/AppScheduler.java @@ -11,6 +11,7 @@ import com.cronutils.parser.CronParser; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Properties; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -206,7 +207,11 @@ public class AppScheduler { private JobDetail jobBuilder(App app, String jobIdentity) throws ClassNotFoundException { JobDataMap dataMap = new JobDataMap(); dataMap.put(APP_NAME, app.getName()); - dataMap.put("triggerType", app.getAppSchedule().getScheduleTimeline().value()); + dataMap.put( + "triggerType", + Optional.ofNullable(app.getAppSchedule()) + .map(v -> v.getScheduleTimeline().value()) + .orElse(null)); Class clz = (Class) Class.forName(app.getClassName()); JobBuilder jobBuilder = 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 231280e296a..6eefd68cc87 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 @@ -70,6 +70,7 @@ import org.openmetadata.schema.auth.PasswordResetToken; import org.openmetadata.schema.auth.PersonalAccessToken; import org.openmetadata.schema.auth.RefreshToken; import org.openmetadata.schema.auth.TokenType; +import org.openmetadata.schema.auth.collate.SupportToken; import org.openmetadata.schema.configuration.AssetCertificationSettings; import org.openmetadata.schema.configuration.WorkflowSettings; import org.openmetadata.schema.dataInsight.DataInsightChart; @@ -5764,6 +5765,7 @@ public interface CollectionDAO { case PASSWORD_RESET -> JsonUtils.readValue(json, PasswordResetToken.class); case REFRESH_TOKEN -> JsonUtils.readValue(json, RefreshToken.class); case PERSONAL_ACCESS_TOKEN -> JsonUtils.readValue(json, PersonalAccessToken.class); + case SUPPORT_TOKEN -> JsonUtils.readValue(json, SupportToken.class); }; } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jobs/GenericBackgroundWorker.java b/openmetadata-service/src/main/java/org/openmetadata/service/jobs/GenericBackgroundWorker.java index 684aa9a09b6..e5887b92ba3 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jobs/GenericBackgroundWorker.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jobs/GenericBackgroundWorker.java @@ -15,7 +15,7 @@ public class GenericBackgroundWorker implements Managed { private static final int INITIAL_BACKOFF_SECONDS = 1; private static final int MAX_BACKOFF_SECONDS = 600; // 10 minutes - private static final int NO_JOB_SLEEP_SECONDS = 10; // Sleep if no jobs are available + public static final int NO_JOB_SLEEP_SECONDS = 10; // Sleep if no jobs are available private final JobDAO jobDao; private final JobHandlerRegistry handlerRegistry; diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jobs/JobDAO.java b/openmetadata-service/src/main/java/org/openmetadata/service/jobs/JobDAO.java index 73bb9997133..29fa5b61930 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jobs/JobDAO.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jobs/JobDAO.java @@ -26,37 +26,50 @@ public interface JobDAO { default long insertJob( BackgroundJob.JobType jobType, JobHandler handler, String jobArgs, String createdBy) { + return insertJob(jobType, handler, jobArgs, createdBy, null); + } + + default long insertJob( + BackgroundJob.JobType jobType, + JobHandler handler, + String jobArgs, + String createdBy, + Long runAt) { try { JsonUtils.readTree(jobArgs); } catch (Exception e) { throw new IllegalArgumentException("jobArgs must be a valid JSON string"); } return insertJobInternal( - jobType.name(), handler.getClass().getSimpleName(), jobArgs, createdBy); + jobType.name(), handler.getClass().getSimpleName(), jobArgs, createdBy, runAt); } @ConnectionAwareSqlUpdate( value = - "INSERT INTO background_jobs (jobType, methodName, jobArgs, createdBy) " - + "VALUES (:jobType, :methodName, :jobArgs, :createdBy)", + "INSERT INTO background_jobs (jobType, methodName, jobArgs, createdBy, runAt) " + + "VALUES (:jobType, :methodName, :jobArgs, :createdBy, :runAt)", connectionType = MYSQL) @ConnectionAwareSqlUpdate( value = - "INSERT INTO background_jobs (jobType, methodName, jobArgs,createdBy) VALUES (:jobType, :methodName, :jobArgs::jsonb,:createdBy) ", + "INSERT INTO background_jobs (jobType, methodName, jobArgs,createdBy,runAt) VALUES (:jobType, :methodName, :jobArgs::jsonb,:createdBy,:runAt) ", connectionType = POSTGRES) @GetGeneratedKeys long insertJobInternal( @Bind("jobType") String jobType, @Bind("methodName") String methodName, @Bind("jobArgs") String jobArgs, - @Bind("createdBy") String createdBy); + @Bind("createdBy") String createdBy, + @Bind("runAt") Long runAt); default Optional fetchPendingJob() throws BackgroundJobException { return Optional.ofNullable(fetchPendingJobInternal()); } @SqlQuery( - "SELECT id,jobType,methodName,jobArgs,status,createdAt,updatedAt,createdBy FROM background_jobs WHERE status = 'PENDING' ORDER BY createdAt LIMIT 1") + "SELECT id,jobType,methodName,jobArgs,status,createdAt,updatedAt,createdBy,runAt FROM background_jobs" + + " WHERE status = 'PENDING'" + + " AND COALESCE(runAt, 0) <= UNIX_TIMESTAMP(NOW(3)) * 1000" + + " ORDER BY createdAt LIMIT 1") @RegisterRowMapper(BackgroundJobMapper.class) BackgroundJob fetchPendingJobInternal() throws StatementException; diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jobs/JobHandlerRegistry.java b/openmetadata-service/src/main/java/org/openmetadata/service/jobs/JobHandlerRegistry.java index 2b64249742c..61cc34fb031 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jobs/JobHandlerRegistry.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jobs/JobHandlerRegistry.java @@ -10,10 +10,17 @@ public class JobHandlerRegistry { private final Map handlers = new HashMap<>(); public void register(String methodName, JobHandler handler) { - LOG.info("Registering background job handler for: {}", handler.getClass().getSimpleName()); + LOG.info( + "Registering background job handler for: {} -> {}", + handler.getClass().getSimpleName(), + handler.getClass().getCanonicalName()); handlers.put(methodName, handler); } + public void register(JobHandler handler) { + register(handler.getClass().getSimpleName(), handler); + } + public JobHandler getHandler(BackgroundJob job) { String methodName = job.getMethodName(); Long jobId = job.getId(); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppResource.java index 0d86b8e24c8..4791fd1e017 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppResource.java @@ -107,7 +107,11 @@ public class AppResource extends EntityResource { static final String FIELDS = "owners"; private SearchRepository searchRepository; public static final List SCHEDULED_TYPES = - List.of(ScheduleType.Scheduled, ScheduleType.ScheduledOrManual, ScheduleType.NoSchedule); + List.of( + ScheduleType.Scheduled, + ScheduleType.ScheduledOrManual, + ScheduleType.NoSchedule, + ScheduleType.OnlyManual); private final AppMapper mapper = new AppMapper(); @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/security/JwtFilter.java b/openmetadata-service/src/main/java/org/openmetadata/service/security/JwtFilter.java index ac11ecbda77..0faf3700a8e 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/security/JwtFilter.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/security/JwtFilter.java @@ -99,7 +99,8 @@ public class JwtFilter implements ContainerRequestFilter { "v1/users/generatePasswordResetLink", "v1/users/password/reset", "v1/users/login", - "v1/users/refresh"); + "v1/users/refresh", + "v1/collate/apps/support/login"); @SuppressWarnings("unused") private JwtFilter() {} diff --git a/openmetadata-service/src/test/java/org/openmetadata/jobs/BackgroundJobWorkerTest.java b/openmetadata-service/src/test/java/org/openmetadata/jobs/BackgroundJobWorkerTest.java index 50afd197845..e5f0b604b4d 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/jobs/BackgroundJobWorkerTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/jobs/BackgroundJobWorkerTest.java @@ -6,11 +6,10 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; 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 static org.openmetadata.service.jobs.GenericBackgroundWorker.NO_JOB_SLEEP_SECONDS; import static org.openmetadata.service.security.SecurityUtil.getPrincipalName; import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS; -import java.io.IOException; -import java.net.URISyntaxException; import java.security.SecureRandom; import java.util.List; import java.util.Map; @@ -31,6 +30,7 @@ import org.openmetadata.service.OpenMetadataApplicationTest; import org.openmetadata.service.jdbi3.CollectionDAO; import org.openmetadata.service.jobs.BackgroundJobException; import org.openmetadata.service.jobs.EnumCleanupHandler; +import org.openmetadata.service.jobs.GenericBackgroundWorker; import org.openmetadata.service.jobs.JobDAO; import org.openmetadata.service.jobs.JobHandler; import org.openmetadata.service.jobs.JobHandlerRegistry; @@ -50,11 +50,12 @@ public class BackgroundJobWorkerTest extends OpenMetadataApplicationTest { public static CustomProperty customPropertyMulti; public static CustomProperty customPropertySingle; + public static GenericBackgroundWorker worker; public static Table TABLE4; @BeforeAll - public static void setup(TestInfo test) throws IOException, URISyntaxException { + public static void setup(TestInfo test) throws Exception { registry = new JobHandlerRegistry(); jobDAO = Entity.getJobDAO(); collectionDao = Entity.getCollectionDAO(); @@ -78,7 +79,7 @@ public class BackgroundJobWorkerTest extends OpenMetadataApplicationTest { .withConfig( Map.of( "values", - List.of("single1", "single2", "single3", "single4", "\"single5\""), + List.of("\"single5\"", "single1", "single2", "single3", "single4"), "multiSelect", false))); @@ -92,7 +93,7 @@ public class BackgroundJobWorkerTest extends OpenMetadataApplicationTest { .withConfig( Map.of( "values", - List.of("multi1", "multi2", "multi3", "multi4", "\"multi5\""), + List.of("\"multi5\"", "multi1", "multi2", "multi3", "multi4"), "multiSelect", true))); CustomProperty[] customProperties = {customPropertySingle, customPropertyMulti}; @@ -173,7 +174,6 @@ public class BackgroundJobWorkerTest extends OpenMetadataApplicationTest { @Test public final void testBackgroundJobTriggerWithValidArgs() { - EnumCleanupArgs enumCleanupArgs = new EnumCleanupArgs() .withPropertyName(customPropertyMulti.getName()) @@ -204,4 +204,58 @@ public class BackgroundJobWorkerTest extends OpenMetadataApplicationTest { assertEquals(enumCleanupArgs, actualArgs, "Job arguments should match"); assertEquals(job.getCreatedBy(), fetchedJob.getCreatedBy(), "Created by should match"); } + + @Test + public final void testDelayedJobTrigger() throws InterruptedException { + // Create a delayed job for enum cleanup + EnumCleanupArgs enumCleanupArgs = + new EnumCleanupArgs() + .withPropertyName(customPropertyMulti.getName()) + .withRemovedEnumKeys(List.of()) + .withEntityType("table"); + String jobArgs = JsonUtils.pojoToJson(enumCleanupArgs); + String createdBy = "admin"; + long delayInMillis = 100; // 100ms delay + + long jobId = + Entity.getJobDAO() + .insertJob( + BackgroundJob.JobType.CUSTOM_PROPERTY_ENUM_CLEANUP, + new EnumCleanupHandler(collectionDao), + jobArgs, + createdBy, + System.currentTimeMillis() + delayInMillis); + + Optional fetchedJobOptional = Entity.getJobDAO().fetchJobById(jobId); + assertTrue(fetchedJobOptional.isPresent(), "Delayed job should be present"); + + BackgroundJob fetchedJob = fetchedJobOptional.get(); + assertEquals( + BackgroundJob.JobType.CUSTOM_PROPERTY_ENUM_CLEANUP, + fetchedJob.getJobType(), + "Job type should match"); + assertEquals("EnumCleanupHandler", fetchedJob.getMethodName(), "Method name should match"); + assertEquals(createdBy, fetchedJob.getCreatedBy(), "Created by should match"); + + // Verify the job arguments + EnumCleanupArgs actualArgs = + JsonUtils.readValue(JsonUtils.pojoToJson(fetchedJob.getJobArgs()), EnumCleanupArgs.class); + assertEquals(enumCleanupArgs, actualArgs, "Job arguments should match"); + + // Verify job is not executed immediately + Thread.sleep(delayInMillis - 50); + Optional jobAfterShortWait = Entity.getJobDAO().fetchJobById(jobId); + assertTrue(jobAfterShortWait.isPresent(), "Job should still exist after short wait"); + assertEquals( + BackgroundJob.Status.PENDING, + jobAfterShortWait.get().getStatus(), + "Job should not be completed yet"); + + // Wait for the next run cycle + Thread.sleep(delayInMillis + NO_JOB_SLEEP_SECONDS); + Optional jobAfterDelay = Entity.getJobDAO().fetchJobById(jobId); + assertTrue(jobAfterDelay.isPresent(), "Job should still exist after delay"); + assertEquals( + BackgroundJob.Status.COMPLETED, jobAfterDelay.get().getStatus(), "Job should be completed"); + } } diff --git a/openmetadata-spec/src/main/resources/json/schema/auth/emailVerificationToken.json b/openmetadata-spec/src/main/resources/json/schema/auth/emailVerificationToken.json index ea47256e4ab..9f85d3fd6c5 100644 --- a/openmetadata-spec/src/main/resources/json/schema/auth/emailVerificationToken.json +++ b/openmetadata-spec/src/main/resources/json/schema/auth/emailVerificationToken.json @@ -15,7 +15,8 @@ "REFRESH_TOKEN", "EMAIL_VERIFICATION", "PASSWORD_RESET", - "PERSONAL_ACCESS_TOKEN" + "PERSONAL_ACCESS_TOKEN", + "SUPPORT_TOKEN" ] } }, diff --git a/openmetadata-spec/src/main/resources/json/schema/auth/supportToken.json b/openmetadata-spec/src/main/resources/json/schema/auth/supportToken.json new file mode 100644 index 00000000000..bd7b6f569e3 --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/auth/supportToken.json @@ -0,0 +1,42 @@ +{ + "$id": "https://open-metadata.org/schema/auth/supportToken.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SupportToken", + "description": "This schema defines an access token used for support purposes. It is used only in Collate.", + "type": "object", + "javaType": "org.openmetadata.schema.auth.collate.SupportToken", + "javaInterfaces": ["org.openmetadata.schema.TokenInterface"], + "properties": { + "token": { + "description": "Unique Refresh Token for user", + "$ref": "../type/basic.json#/definitions/uuid" + }, + "tokenName": { + "description": "Name of the token", + "type": "string" + }, + "userId": { + "description": "User Id of the User this refresh token is given to", + "$ref": "../type/basic.json#/definitions/uuid" + }, + "tokenType": { + "description": "Token Type", + "$ref": "./emailVerificationToken.json#/definitions/tokenType", + "default": "SUPPORT_TOKEN" + }, + "expiryDate": { + "description": "Expiry Date-Time of the token", + "$ref": "../type/basic.json#/definitions/timestamp" + }, + "jwtToken": { + "description": "JWT Auth Token.", + "type": "string" + } + }, + "required": [ + "token", + "userId", + "expiryDate" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/applications/app.json b/openmetadata-spec/src/main/resources/json/schema/entity/applications/app.json index 8fcc736786c..0d318462563 100644 --- a/openmetadata-spec/src/main/resources/json/schema/entity/applications/app.json +++ b/openmetadata-spec/src/main/resources/json/schema/entity/applications/app.json @@ -3,7 +3,9 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "App", "javaType": "org.openmetadata.schema.entity.app.App", - "javaInterfaces": ["org.openmetadata.schema.EntityInterface"], + "javaInterfaces": [ + "org.openmetadata.schema.EntityInterface" + ], "description": "This schema defines the applications for Open-Metadata.", "type": "object", "definitions": { @@ -15,20 +17,29 @@ "Live", "Scheduled", "ScheduledOrManual", - "NoSchedule" + "NoSchedule", + "OnlyManual" ], "javaEnums": [ { - "name": "Live" + "name": "Live", + "description": "An app that has other trigger mechanisms." }, { - "name": "Scheduled" + "name": "Scheduled", + "description": "An app that has a schedule and cannot be run manually." }, { - "name": "ScheduledOrManual" + "name": "ScheduledOrManual", + "description": "An app that has a schedule and can be run manually." }, { - "name": "NoSchedule" + "name": "NoSchedule", + "description": "An app that has no schedule and cannot be run manually." + }, + { + "name": "OnlyManual", + "description": "An app that has no schedule but can be run manually." } ] }, @@ -36,7 +47,14 @@ "javaType": "org.openmetadata.schema.entity.app.ScheduleTimeline", "description": "This schema defines the Application ScheduleTimeline Options", "type": "string", - "enum": ["Hourly"," Daily", "Weekly", "Monthly", "Custom", "None"], + "enum": [ + "Hourly", + "Daily", + "Weekly", + "Monthly", + "Custom", + "None" + ], "default": "Weekly" }, "appSchedule": { @@ -51,7 +69,9 @@ "type": "string" } }, - "required": ["scheduleTimeline"], + "required": [ + "scheduleTimeline" + ], "additionalProperties": false }, "appType": { @@ -173,7 +193,7 @@ "type": "boolean", "default": false }, - "provider" : { + "provider": { "$ref": "../../type/basic.json#/definitions/providerType" }, "developer": { @@ -267,12 +287,12 @@ "appScreenshots": { "description": "Application Screenshots.", "type": "array", - "items":{ + "items": { "type": "string" }, "uniqueItems": true }, - "domain" : { + "domain": { "description": "Domain the asset belongs to. When not set, the asset inherits the domain from the parent it belongs to.", "$ref": "../../type/entityReference.json" }, @@ -283,7 +303,7 @@ }, "eventSubscriptions": { "description": "Event Subscriptions for the Application.", - "$ref": "../../type/entityReferenceList.json" + "$ref": "../../type/entityReferenceList.json" } }, "additionalProperties": false, diff --git a/openmetadata-spec/src/main/resources/json/schema/jobs/backgroundJob.json b/openmetadata-spec/src/main/resources/json/schema/jobs/backgroundJob.json index f5e95e69d8f..e74483040e8 100644 --- a/openmetadata-spec/src/main/resources/json/schema/jobs/backgroundJob.json +++ b/openmetadata-spec/src/main/resources/json/schema/jobs/backgroundJob.json @@ -12,7 +12,7 @@ }, "jobType": { "type": "string", - "enum": ["CUSTOM_PROPERTY_ENUM_CLEANUP"], + "enum": ["CUSTOM_PROPERTY_ENUM_CLEANUP", "DELETE_ENTITY", "DELETE_TOKEN"], "description": "Type of the job." }, "methodName": { @@ -23,10 +23,18 @@ "oneOf": [ { "$ref": "./enumCleanupArgs.json" + }, + { + "type": "object", + "additionalProperties": true } ], "description": "Object containing job arguments." }, + "runAt": { + "description": "Timestamp when the job was run in Unix epoch time milliseconds (default: as soon as possible).", + "$ref": "../type/basic.json#/definitions/timestamp" + }, "status": { "type": "string", "enum": ["COMPLETED", "FAILED", "RUNNING","PENDING"], diff --git a/openmetadata-ui/src/main/resources/ui/jest.config.js b/openmetadata-ui/src/main/resources/ui/jest.config.js index 5dd8bd4c59f..7b97307af99 100644 --- a/openmetadata-ui/src/main/resources/ui/jest.config.js +++ b/openmetadata-ui/src/main/resources/ui/jest.config.js @@ -82,14 +82,17 @@ module.exports = { moduleDirectories: ['node_modules', 'src'], reporters: [ - "default", - ["jest-junit", { - outputDirectory: "../../../../target/test-reports", - outputName: "jest-junit.xml", - classNameTemplate: "{classname}", - titleTemplate: "{title}", - ancestorSeparator: " › ", - usePathForSuiteName: "true" - }] - ] + 'default', + [ + 'jest-junit', + { + outputDirectory: '../../../../target/test-reports', + outputName: 'jest-junit.xml', + classNameTemplate: '{classname}', + titleTemplate: '{title}', + ancestorSeparator: ' › ', + usePathForSuiteName: 'true', + }, + ], + ], }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppContainer/AppContainer.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppContainer/AppContainer.test.tsx index a57b8c74583..3a99197808b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppContainer/AppContainer.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppContainer/AppContainer.test.tsx @@ -26,10 +26,6 @@ jest.mock('../../hooks/useApplicationStore', () => { }; }); -jest.mock('../../utils/AuthProvider.util', () => ({ - isProtectedRoute: jest.fn().mockReturnValue(true), -})); - jest.mock( '../Settings/Applications/ApplicationsProvider/ApplicationsProvider', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppContainer/AppContainer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppContainer/AppContainer.tsx index 32d44444758..a6af78897a6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppContainer/AppContainer.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppContainer/AppContainer.tsx @@ -21,7 +21,6 @@ import { useApplicationStore } from '../../hooks/useApplicationStore'; import { getLimitConfig } from '../../rest/limitsAPI'; import { getSettingsByType } from '../../rest/settingConfigAPI'; import applicationRoutesClass from '../../utils/ApplicationRoutesClassBase'; -import { isProtectedRoute } from '../../utils/AuthProvider.util'; import { LimitBanner } from '../common/LimitBanner/LimitBanner'; import LeftSidebar from '../MyData/LeftSidebar/LeftSidebar.component'; import NavBar from '../NavBar/NavBar'; @@ -75,7 +74,8 @@ const AppContainer = () => { {/* Render main content */} {/* Render Appbar */} - {isProtectedRoute(location.pathname) && isAuthenticated ? ( + {applicationRoutesClass.isProtectedRoute(location.pathname) && + isAuthenticated ? ( ) : null} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AppRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AppRouter.tsx index aadd80980dd..72967ed2de1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AppRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AppRouter.tsx @@ -24,13 +24,15 @@ import PageNotFound from '../../pages/PageNotFound/PageNotFound'; import SignUpPage from '../../pages/SignUp/SignUpPage'; import AppContainer from '../AppContainer/AppContainer'; import Loader from '../common/Loader/Loader'; -import { UnAuthenticatedAppRouter } from './UnAuthenticatedAppRouter'; import { LogoutPage } from '../../pages/LogoutPage/LogoutPage'; import SamlCallback from '../../pages/SamlCallback'; +import applicationRoutesClass from '../../utils/ApplicationRoutesClassBase'; const AppRouter = () => { const location = useCustomLocation(); + const UnAuthenticatedAppRouter = + applicationRoutesClass.getUnAuthenticatedRouteElements(); // web analytics instance const analytics = useAnalytics(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/UnAuthenticatedAppRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/UnAuthenticatedAppRouter.tsx index 0b5ba794da4..b440fe74272 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/UnAuthenticatedAppRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/UnAuthenticatedAppRouter.tsx @@ -19,7 +19,7 @@ import { useApplicationStore } from '../../hooks/useApplicationStore'; import useCustomLocation from '../../hooks/useCustomLocation/useCustomLocation'; import PageNotFound from '../../pages/PageNotFound/PageNotFound'; import AccountActivationConfirmation from '../../pages/SignUp/account-activation-confirmation.component'; -import { isProtectedRoute } from '../../utils/AuthProvider.util'; +import applicationRoutesClass from '../../utils/ApplicationRoutesClassBase'; import Auth0Callback from '../Auth/AppCallbacks/Auth0Callback/Auth0Callback'; import withSuspenseFallback from './withSuspenseFallback'; @@ -64,7 +64,7 @@ export const UnAuthenticatedAppRouter = () => { } }, [authConfig?.provider]); - if (isProtectedRoute(location.pathname)) { + if (applicationRoutesClass.isProtectedRoute(location.pathname)) { return ; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx index f60037b4e16..e5e2b42dd6e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx @@ -64,13 +64,13 @@ import { fetchAuthorizerConfig, } from '../../../rest/miscAPI'; import { getLoggedInUser } from '../../../rest/userAPI'; +import applicationRoutesClass from '../../../utils/ApplicationRoutesClassBase'; import TokenService from '../../../utils/Auth/TokenService/TokenServiceUtil'; import { extractDetailsFromToken, getAuthConfig, getUrlPathnameExpiry, getUserManagerConfig, - isProtectedRoute, prepareUserProfileFromClaims, } from '../../../utils/AuthProvider.util'; import { @@ -209,7 +209,7 @@ export const AuthProvider = ({ }, []); const handledVerifiedUser = () => { - if (!isProtectedRoute(location.pathname)) { + if (!applicationRoutesClass.isProtectedRoute(location.pathname)) { history.push(ROUTES.HOME); } }; @@ -404,7 +404,7 @@ export const AuthProvider = ({ * Stores redirect URL for successful login */ const handleStoreProtectedRedirectPath = useCallback(() => { - if (isProtectedRoute(location.pathname)) { + if (applicationRoutesClass.isProtectedRoute(location.pathname)) { storeRedirectPath(location.pathname); } }, [location.pathname, storeRedirectPath]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.tsx b/openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.tsx index 3f1d678d5da..bf80d818241 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.tsx @@ -58,12 +58,16 @@ import { useTourProvider } from '../../context/TourProvider/TourProvider'; import { useWebSocketConnector } from '../../context/WebSocketProvider/WebSocketProvider'; import { EntityTabs, EntityType } from '../../enums/entity.enum'; import { EntityReference } from '../../generated/entity/type'; -import { BackgroundJob, JobType } from '../../generated/jobs/backgroundJob'; +import { + BackgroundJob, + EnumCleanupArgs, + JobType, +} from '../../generated/jobs/backgroundJob'; import { useCurrentUserPreferences } from '../../hooks/currentUserStore/useCurrentUserStore'; import useCustomLocation from '../../hooks/useCustomLocation/useCustomLocation'; import { useDomainStore } from '../../hooks/useDomainStore'; import { getVersion } from '../../rest/miscAPI'; -import { isProtectedRoute } from '../../utils/AuthProvider.util'; +import applicationRoutesClass from '../../utils/ApplicationRoutesClassBase'; import brandClassBase from '../../utils/BrandData/BrandClassBase'; import { hasNotificationPermission, @@ -259,6 +263,18 @@ const NavBar = () => { const { jobArgs, status, jobType } = backgroundJobData; if (jobType === JobType.CustomPropertyEnumCleanup) { + const enumCleanupArgs = jobArgs as EnumCleanupArgs; + if (!enumCleanupArgs.entityType) { + showErrorToast( + { + isAxiosError: true, + message: 'Invalid job arguments: entityType is required', + } as AxiosError, + t('message.unexpected-error') + ); + + break; + } body = t('message.custom-property-update', { propertyName: jobArgs.propertyName, entityName: jobArgs.entityType, @@ -267,7 +283,7 @@ const NavBar = () => { path = getSettingPath( GlobalSettingsMenuCategory.CUSTOM_PROPERTIES, - getCustomPropertyEntityPathname(jobArgs.entityType) + getCustomPropertyEntityPathname(enumCleanupArgs.entityType) ); } @@ -303,7 +319,10 @@ const NavBar = () => { } const handleDocumentVisibilityChange = async () => { - if (isProtectedRoute(location.pathname) && isTourRoute) { + if ( + applicationRoutesClass.isProtectedRoute(location.pathname) && + isTourRoute + ) { return; } const newVersion = await getVersion(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.component.tsx index a07ecafe65b..3c8e7d5ae01 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.component.tsx @@ -54,11 +54,9 @@ const AppSchedule = ({ const { config } = useLimitStore(); const showRunNowButton = useMemo(() => { - if (appData && appData.scheduleType === ScheduleType.ScheduledOrManual) { - return true; - } - - return false; + return [ScheduleType.ScheduledOrManual, ScheduleType.OnlyManual].includes( + appData?.scheduleType + ); }, [appData]); const { pipelineSchedules } = diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/auth/emailVerificationToken.ts b/openmetadata-ui/src/main/resources/ui/src/generated/auth/emailVerificationToken.ts index 23ef4003abc..3b033ec1caa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/auth/emailVerificationToken.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/auth/emailVerificationToken.ts @@ -54,4 +54,5 @@ export enum TokenType { PasswordReset = "PASSWORD_RESET", PersonalAccessToken = "PERSONAL_ACCESS_TOKEN", RefreshToken = "REFRESH_TOKEN", + SupportToken = "SUPPORT_TOKEN", } diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/auth/supporToken.ts b/openmetadata-ui/src/main/resources/ui/src/generated/auth/supporToken.ts new file mode 100644 index 00000000000..33064e1ee36 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/generated/auth/supporToken.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2025 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. + */ +/** + * This schema defines an access token used for support purposes. It is used only in Collate. + */ +export interface SupporToken { + /** + * Expiry Date-Time of the token + */ + expiryDate: number; + /** + * JWT Auth Token. + */ + jwtToken?: string; + /** + * Unique Refresh Token for user + */ + token: string; + /** + * Name of the token + */ + tokenName?: string; + /** + * Token Type + */ + tokenType?: TokenType; + /** + * User Id of the User this refresh token is given to + */ + userId: string; +} + +/** + * Token Type + * + * Different Type of User token + */ +export enum TokenType { + EmailVerification = "EMAIL_VERIFICATION", + PasswordReset = "PASSWORD_RESET", + PersonalAccessToken = "PERSONAL_ACCESS_TOKEN", + RefreshToken = "REFRESH_TOKEN", + SupportToken = "SUPPORT_TOKEN", +} diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/entity/applications/app.ts b/openmetadata-ui/src/main/resources/ui/src/generated/entity/applications/app.ts index 058addafc7d..1abd8622cf5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/entity/applications/app.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/entity/applications/app.ts @@ -1324,6 +1324,7 @@ export interface ExecutionContext { export enum ScheduleType { Live = "Live", NoSchedule = "NoSchedule", + OnlyManual = "OnlyManual", Scheduled = "Scheduled", ScheduledOrManual = "ScheduledOrManual", } diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/jobs/backgroundJob.ts b/openmetadata-ui/src/main/resources/ui/src/generated/jobs/backgroundJob.ts index 95e4bf4dfce..69cf560b48a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/jobs/backgroundJob.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/jobs/backgroundJob.ts @@ -39,6 +39,11 @@ export interface BackgroundJob { * JobHandler name of the method that will be executed for this job. */ methodName: string; + /** + * Timestamp when the job was run in Unix epoch time milliseconds (default: as soon as + * possible). + */ + runAt?: number; /** * Current status of the job. */ @@ -58,15 +63,16 @@ export interface EnumCleanupArgs { /** * Type of the entity. */ - entityType: string; + entityType?: string; /** * Name of the property. */ - propertyName: string; + propertyName?: string; /** * List of removed enum keys. */ - removedEnumKeys: string[]; + removedEnumKeys?: string[]; + [property: string]: any; } /** @@ -74,6 +80,8 @@ export interface EnumCleanupArgs { */ export enum JobType { CustomPropertyEnumCleanup = "CUSTOM_PROPERTY_ENUM_CLEANUP", + DeleteEntity = "DELETE_ENTITY", + DeleteToken = "DELETE_TOKEN", } /** diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ApplicationRoutesClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ApplicationRoutesClassBase.ts index ea902af16da..a63d168eb6a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ApplicationRoutesClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ApplicationRoutesClassBase.ts @@ -13,11 +13,37 @@ import { FC } from 'react'; import AuthenticatedAppRouter from '../components/AppRouter/AuthenticatedAppRouter'; +import { UnAuthenticatedAppRouter } from '../components/AppRouter/UnAuthenticatedAppRouter'; +import { ROUTES } from '../constants/constants'; class ApplicationRoutesClassBase { public getRouteElements(): FC { return AuthenticatedAppRouter; } + + public getUnAuthenticatedRouteElements(): FC { + return UnAuthenticatedAppRouter; + } + + public isProtectedRoute(pathname: string): boolean { + return ( + [ + ROUTES.SIGNUP, + ROUTES.SIGNIN, + ROUTES.FORGOT_PASSWORD, + ROUTES.CALLBACK, + ROUTES.SILENT_CALLBACK, + ROUTES.SAML_CALLBACK, + ROUTES.REGISTER, + ROUTES.RESET_PASSWORD, + ROUTES.ACCOUNT_ACTIVATION, + ROUTES.HOME, + ROUTES.AUTH_CALLBACK, + ROUTES.NOT_FOUND, + ROUTES.LOGOUT, + ].indexOf(pathname) === -1 + ); + } } const applicationRoutesClass = new ApplicationRoutesClassBase(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider.util.ts b/openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider.util.ts index d2c591e5015..9af5e45a1bb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider.util.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider.util.ts @@ -296,26 +296,6 @@ export const getNameFromUserData = ( return { name: userName, email: email, picture: user.picture }; }; -export const isProtectedRoute = (pathname: string) => { - return ( - [ - ROUTES.SIGNUP, - ROUTES.SIGNIN, - ROUTES.FORGOT_PASSWORD, - ROUTES.CALLBACK, - ROUTES.SILENT_CALLBACK, - ROUTES.SAML_CALLBACK, - ROUTES.REGISTER, - ROUTES.RESET_PASSWORD, - ROUTES.ACCOUNT_ACTIVATION, - ROUTES.HOME, - ROUTES.AUTH_CALLBACK, - ROUTES.NOT_FOUND, - ROUTES.LOGOUT, - ].indexOf(pathname) === -1 - ); -}; - export const isTourRoute = (pathname: string) => { return pathname === ROUTES.TOUR; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/i18next/i18nextUtil.ts b/openmetadata-ui/src/main/resources/ui/src/utils/i18next/i18nextUtil.ts index 745ce84356a..54174343eb3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/i18next/i18nextUtil.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/i18next/i18nextUtil.ts @@ -28,8 +28,8 @@ import ptBR from '../../locale/languages/pt-br.json'; import ptPT from '../../locale/languages/pt-pt.json'; import ruRU from '../../locale/languages/ru-ru.json'; import thTH from '../../locale/languages/th-th.json'; -import zhCN from '../../locale/languages/zh-cn.json'; import trTR from '../../locale/languages/tr-tr.json'; +import zhCN from '../../locale/languages/zh-cn.json'; export enum SupportedLocales { English = 'en-US',