mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-15 17:46:39 +00:00
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 <karanh37@gmail.com> Co-authored-by: Pere Miquel Brull <peremiquelbrull@gmail.com>
This commit is contained in:
parent
6ea630d7ef
commit
baee931b85
@ -0,0 +1,4 @@
|
||||
ALTER TABLE background_jobs
|
||||
ADD COLUMN runAt BIGINT;
|
||||
|
||||
CREATE INDEX background_jobs_run_at_index ON background_jobs(runAt);
|
||||
@ -0,0 +1,4 @@
|
||||
ALTER TABLE background_jobs
|
||||
ADD COLUMN runAt BIGINT;
|
||||
|
||||
CREATE INDEX background_jobs_run_at_index ON background_jobs(runAt);
|
||||
@ -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<OpenMetadataApplication
|
||||
registerEventFilter(catalogConfig, environment);
|
||||
environment.lifecycle().manage(new ManagedShutdown());
|
||||
|
||||
JobHandlerRegistry registry = new JobHandlerRegistry();
|
||||
registry.register("EnumCleanupHandler", new EnumCleanupHandler(getDao(jdbi)));
|
||||
JobHandlerRegistry registry = getJobHandlerRegistry();
|
||||
environment
|
||||
.lifecycle()
|
||||
.manage(new GenericBackgroundWorker(jdbi.onDemand(JobDAO.class), registry));
|
||||
@ -284,6 +284,12 @@ public class OpenMetadataApplication extends Application<OpenMetadataApplication
|
||||
}
|
||||
}
|
||||
|
||||
protected @NotNull JobHandlerRegistry getJobHandlerRegistry() {
|
||||
JobHandlerRegistry registry = new JobHandlerRegistry();
|
||||
registry.register("EnumCleanupHandler", new EnumCleanupHandler(getDao(jdbi)));
|
||||
return registry;
|
||||
}
|
||||
|
||||
private void registerHealthCheckJobs(OpenMetadataApplicationConfig catalogConfig) {
|
||||
ServicesStatusJobHandler healthCheckStatusHandler =
|
||||
ServicesStatusJobHandler.create(
|
||||
|
||||
@ -8,6 +8,7 @@ import static org.openmetadata.service.resources.apps.AppResource.SCHEDULED_TYPE
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -66,10 +67,12 @@ public class AbstractNativeApplication implements NativeApplication {
|
||||
@Override
|
||||
public void install(String installedBy) {
|
||||
// If the app does not have any Schedule Return without scheduling
|
||||
if (Boolean.TRUE.equals(app.getDeleted()) || (app.getAppSchedule() == null)) {
|
||||
return;
|
||||
}
|
||||
if (app.getAppType().equals(AppType.Internal)
|
||||
if (Boolean.TRUE.equals(app.getDeleted())
|
||||
|| (app.getAppSchedule() == null)
|
||||
|| Set.of(ScheduleType.NoSchedule, ScheduleType.OnlyManual)
|
||||
.contains(app.getScheduleType())) {
|
||||
LOG.debug("App {} does not support scheduling.", app.getName());
|
||||
} else if (app.getAppType().equals(AppType.Internal)
|
||||
&& (SCHEDULED_TYPES.contains(app.getScheduleType()))) {
|
||||
try {
|
||||
ApplicationHandler.getInstance().removeOldJobs(app);
|
||||
@ -101,7 +104,8 @@ public class AbstractNativeApplication implements NativeApplication {
|
||||
@Override
|
||||
public void triggerOnDemand(Map<String, Object> 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
|
||||
|
||||
@ -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<? extends NativeApplication> clz =
|
||||
(Class<? extends NativeApplication>) Class.forName(app.getClassName());
|
||||
JobBuilder jobBuilder =
|
||||
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<BackgroundJob> 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;
|
||||
|
||||
|
||||
@ -10,10 +10,17 @@ public class JobHandlerRegistry {
|
||||
private final Map<String, JobHandler> 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();
|
||||
|
||||
@ -107,7 +107,11 @@ public class AppResource extends EntityResource<App, AppRepository> {
|
||||
static final String FIELDS = "owners";
|
||||
private SearchRepository searchRepository;
|
||||
public static final List<ScheduleType> 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
|
||||
|
||||
@ -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() {}
|
||||
|
||||
@ -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<BackgroundJob> 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<BackgroundJob> 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<BackgroundJob> 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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,8 @@
|
||||
"REFRESH_TOKEN",
|
||||
"EMAIL_VERIFICATION",
|
||||
"PASSWORD_RESET",
|
||||
"PERSONAL_ACCESS_TOKEN"
|
||||
"PERSONAL_ACCESS_TOKEN",
|
||||
"SUPPORT_TOKEN"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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"],
|
||||
|
||||
@ -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',
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
@ -26,10 +26,6 @@ jest.mock('../../hooks/useApplicationStore', () => {
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../utils/AuthProvider.util', () => ({
|
||||
isProtectedRoute: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'../Settings/Applications/ApplicationsProvider/ApplicationsProvider',
|
||||
() => {
|
||||
|
||||
@ -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 */}
|
||||
<Layout>
|
||||
{/* Render Appbar */}
|
||||
{isProtectedRoute(location.pathname) && isAuthenticated ? (
|
||||
{applicationRoutesClass.isProtectedRoute(location.pathname) &&
|
||||
isAuthenticated ? (
|
||||
<NavBar />
|
||||
) : null}
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 <Redirect to={ROUTES.SIGNIN} />;
|
||||
}
|
||||
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 } =
|
||||
|
||||
@ -54,4 +54,5 @@ export enum TokenType {
|
||||
PasswordReset = "PASSWORD_RESET",
|
||||
PersonalAccessToken = "PERSONAL_ACCESS_TOKEN",
|
||||
RefreshToken = "REFRESH_TOKEN",
|
||||
SupportToken = "SUPPORT_TOKEN",
|
||||
}
|
||||
|
||||
@ -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",
|
||||
}
|
||||
@ -1324,6 +1324,7 @@ export interface ExecutionContext {
|
||||
export enum ScheduleType {
|
||||
Live = "Live",
|
||||
NoSchedule = "NoSchedule",
|
||||
OnlyManual = "OnlyManual",
|
||||
Scheduled = "Scheduled",
|
||||
ScheduledOrManual = "ScheduledOrManual",
|
||||
}
|
||||
|
||||
@ -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",
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user