diff --git a/openmetadata-service/pom.xml b/openmetadata-service/pom.xml index 1a7ca345579..e7c3064a045 100644 --- a/openmetadata-service/pom.xml +++ b/openmetadata-service/pom.xml @@ -27,6 +27,7 @@ 5.7.0 3.6.0 3.3.1 + 2.1.1 @@ -367,6 +368,13 @@ test + + io.socket + socket.io-client + ${socket.io-client.version} + test + + org.testcontainers junit-jupiter diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java index 9a7156accca..20323c0d00c 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java @@ -13,6 +13,7 @@ package org.openmetadata.service.resources; +import static javax.ws.rs.client.Entity.entity; import static org.openmetadata.common.utils.CommonUtil.listOrEmpty; import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty; import static org.openmetadata.schema.type.EventType.ENTITY_CREATED; @@ -27,7 +28,9 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.UUID; +import java.util.concurrent.ExecutorService; import javax.json.JsonPatch; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.UriInfo; @@ -52,6 +55,8 @@ import org.openmetadata.service.security.policyevaluator.CreateResourceContext; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; +import org.openmetadata.service.util.AsyncService; +import org.openmetadata.service.util.CSVExportResponse; import org.openmetadata.service.util.EntityUtil; import org.openmetadata.service.util.EntityUtil.Fields; import org.openmetadata.service.util.RestUtil; @@ -59,6 +64,7 @@ import org.openmetadata.service.util.RestUtil.DeleteResponse; import org.openmetadata.service.util.RestUtil.PatchResponse; import org.openmetadata.service.util.RestUtil.PutResponse; import org.openmetadata.service.util.ResultList; +import org.openmetadata.service.util.WebsocketNotificationHandler; @Slf4j public abstract class EntityResource> { @@ -376,11 +382,27 @@ public abstract class EntityResource { + try { + String csvData = + repository.exportToCsv(name, securityContext.getUserPrincipal().getName()); + WebsocketNotificationHandler.sendCsvExportCompleteNotification( + jobId, securityContext, csvData); + } catch (Exception e) { + WebsocketNotificationHandler.sendCsvExportFailedNotification( + jobId, securityContext, e.getMessage()); + } + }); + CSVExportResponse response = new CSVExportResponse(jobId, "Export initiated successfully."); + return Response.accepted().entity(response).type(MediaType.APPLICATION_JSON).build(); } protected CsvImportResult importCsvInternal( diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseResource.java index 63d890e6326..d527cbb1363 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseResource.java @@ -65,6 +65,7 @@ import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; import org.openmetadata.service.security.policyevaluator.OperationContext; +import org.openmetadata.service.util.CSVExportResponse; import org.openmetadata.service.util.ResultList; @Path("/v1/databases") @@ -423,7 +424,7 @@ public class DatabaseResource extends EntityResource { mediaType = "application/json", schema = @Schema(implementation = String.class))) }) - public String exportCsv( + public Response exportCsv( @Context SecurityContext securityContext, @Parameter(description = "Name of the table", schema = @Schema(type = "string")) @PathParam("name") diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryResource.java index a14ac78654c..a8ee809654b 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryResource.java @@ -63,6 +63,7 @@ import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; +import org.openmetadata.service.util.CSVExportResponse; import org.openmetadata.service.util.JsonUtils; import org.openmetadata.service.util.ResultList; @@ -513,9 +514,9 @@ public class GlossaryResource extends EntityResource { content = @Content( mediaType = "application/json", - schema = @Schema(implementation = String.class))) + schema = @Schema(implementation = CSVExportResponse.class))) }) - public String exportCsv(@Context SecurityContext securityContext, @PathParam("name") String name) - throws IOException { + public Response exportCsv( + @Context SecurityContext securityContext, @PathParam("name") String name) throws IOException { return exportCsvInternal(securityContext, name); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java index 9a723ef25a1..58d14ffc9d2 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java @@ -150,6 +150,7 @@ import org.openmetadata.service.security.mask.PIIMasker; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContext; import org.openmetadata.service.security.saml.JwtTokenCacheManager; +import org.openmetadata.service.util.CSVExportResponse; import org.openmetadata.service.util.EntityUtil; import org.openmetadata.service.util.EntityUtil.Fields; import org.openmetadata.service.util.JsonUtils; @@ -1404,9 +1405,9 @@ public class UserResource extends EntityResource { content = @Content( mediaType = "application/json", - schema = @Schema(implementation = String.class))) + schema = @Schema(implementation = CSVExportResponse.class))) }) - public String exportUsersCsv( + public Response exportUsersCsv( @Context SecurityContext securityContext, @Parameter( description = "Name of the team to under which the users are imported to", diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/socket/WebSocketManager.java b/openmetadata-service/src/main/java/org/openmetadata/service/socket/WebSocketManager.java index 8cba7eabdc4..75aa8eddb62 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/socket/WebSocketManager.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/socket/WebSocketManager.java @@ -27,6 +27,7 @@ public class WebSocketManager { public static final String JOB_STATUS_BROADCAST_CHANNEL = "jobStatus"; public static final String MENTION_CHANNEL = "mentionChannel"; public static final String ANNOUNCEMENT_CHANNEL = "announcementChannel"; + public static final String CSV_EXPORT_CHANNEL = "csvExportChannel"; @Getter private final Map> activityFeedEndpoints = diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/AsyncService.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/AsyncService.java new file mode 100644 index 00000000000..1059aa285ca --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/AsyncService.java @@ -0,0 +1,44 @@ +package org.openmetadata.service.util; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +public class AsyncService { + private static AsyncService instance; + private final ExecutorService executorService; + + private AsyncService() { + ThreadFactory threadFactory = + new ThreadFactory() { + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix = "AsyncServicePool-Thread-"; + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement()); + if (t.isDaemon()) t.setDaemon(false); + if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); + return t; + } + }; + executorService = Executors.newFixedThreadPool(20, threadFactory); + } + + public static synchronized AsyncService getInstance() { + if (instance == null) { + instance = new AsyncService(); + } + return instance; + } + + public ExecutorService getExecutorService() { + return executorService; + } + + // Optionally, provide a method to shut down the executor service + public void shutdown() { + executorService.shutdown(); + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/CSVExportMessage.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/CSVExportMessage.java new file mode 100644 index 00000000000..23c3e4b481e --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/CSVExportMessage.java @@ -0,0 +1,20 @@ +package org.openmetadata.service.util; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +public class CSVExportMessage { + @Getter @Setter private String jobId; + @Getter @Setter private String status; + @Getter @Setter private String data; + @Getter @Setter private String error; + + public CSVExportMessage(String jobId, String status, String data, String error) { + this.jobId = jobId; + this.status = status; + this.data = data; + this.error = error; + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/CSVExportResponse.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/CSVExportResponse.java new file mode 100644 index 00000000000..91184943d71 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/CSVExportResponse.java @@ -0,0 +1,16 @@ +package org.openmetadata.service.util; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +public class CSVExportResponse { + @Getter @Setter private String jobId; + @Getter @Setter private String message; + + public CSVExportResponse(String jobId, String message) { + this.jobId = jobId; + this.message = message; + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/WebsocketNotificationHandler.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/WebsocketNotificationHandler.java index 84fc8caf81a..d12dacf71b6 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/util/WebsocketNotificationHandler.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/WebsocketNotificationHandler.java @@ -25,6 +25,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; import lombok.extern.slf4j.Slf4j; import org.openmetadata.schema.entity.feed.Thread; import org.openmetadata.schema.entity.teams.Team; @@ -57,6 +58,15 @@ public class WebsocketNotificationHandler { }); } + public static void sendCsvExportCompleteNotification( + String jobId, SecurityContext securityContext, String csvData) { + CSVExportMessage message = new CSVExportMessage(jobId, "COMPLETED", csvData, null); + String jsonMessage = JsonUtils.pojoToJson(message); + UUID userId = getUserIdFromSecurityContext(securityContext); + WebSocketManager.getInstance() + .sendToOne(userId, WebSocketManager.CSV_EXPORT_CHANNEL, jsonMessage); + } + private void handleNotifications(ContainerResponseContext responseContext) { int responseCode = responseContext.getStatus(); if (responseCode == Response.Status.CREATED.getStatusCode() @@ -149,4 +159,19 @@ public class WebsocketNotificationHandler { } }); } + + public static void sendCsvExportFailedNotification( + String jobId, SecurityContext securityContext, String errorMessage) { + CSVExportMessage message = new CSVExportMessage(jobId, "FAILED", null, errorMessage); + String jsonMessage = JsonUtils.pojoToJson(message); + UUID userId = getUserIdFromSecurityContext(securityContext); + WebSocketManager.getInstance() + .sendToOne(userId, WebSocketManager.CSV_EXPORT_CHANNEL, jsonMessage); + } + + private static UUID getUserIdFromSecurityContext(SecurityContext securityContext) { + String username = securityContext.getUserPrincipal().getName(); + User user = Entity.getCollectionDAO().userDAO().findEntityByName(username); + return user.getId(); + } } 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 3922e5237e7..e32092d39bf 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 @@ -76,6 +76,8 @@ import es.org.elasticsearch.xcontent.NamedXContentRegistry; import es.org.elasticsearch.xcontent.ParseField; import es.org.elasticsearch.xcontent.XContentParser; import es.org.elasticsearch.xcontent.json.JsonXContent; +import io.socket.client.IO; +import io.socket.client.Socket; import java.io.IOException; import java.net.URISyntaxException; import java.time.Duration; @@ -91,6 +93,7 @@ import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; @@ -214,6 +217,8 @@ import org.openmetadata.service.resources.tags.TagResourceTest; import org.openmetadata.service.resources.teams.*; import org.openmetadata.service.search.models.IndexMapping; import org.openmetadata.service.security.SecurityUtil; +import org.openmetadata.service.util.CSVExportMessage; +import org.openmetadata.service.util.CSVExportResponse; import org.openmetadata.service.util.EntityUtil; import org.openmetadata.service.util.FullyQualifiedName; import org.openmetadata.service.util.JsonUtils; @@ -3967,9 +3972,91 @@ public abstract class EntityResourceTest { + System.out.println("Connected to Socket.IO server"); + connectLatch.countDown(); + }) + .on( + "csvExportChannel", + args -> { + receivedMessage[0] = (String) args[0]; + System.out.println("Received message: " + receivedMessage[0]); + messageLatch.countDown(); + socket.disconnect(); + }) + .on( + Socket.EVENT_CONNECT_ERROR, + args -> { + System.err.println("Socket.IO connect error: " + args[0]); + connectLatch.countDown(); + messageLatch.countDown(); + }) + .on( + Socket.EVENT_DISCONNECT, + args -> { + System.out.println("Disconnected from Socket.IO server"); + }); + + socket.connect(); + if (!connectLatch.await(10, TimeUnit.SECONDS)) { + fail("Could not connect to Socket.IO server"); + } + + // Initiate the export after connection is established + String jobId = initiateExport(entityName); + + if (!messageLatch.await(30, TimeUnit.SECONDS)) { + fail("Did not receive CSV data via Socket.IO within the expected time."); + } + + String receivedJson = receivedMessage[0]; + if (receivedJson == null) { + fail("Received message is null."); + } + + CSVExportMessage csvExportMessage = JsonUtils.readValue(receivedJson, CSVExportMessage.class); + if ("COMPLETED".equals(csvExportMessage.getStatus())) { + return csvExportMessage.getData(); + } else if ("FAILED".equals(csvExportMessage.getStatus())) { + fail("CSV export failed: " + csvExportMessage.getError()); + } else { + fail("Unknown status received: " + csvExportMessage.getStatus()); + } + return null; + } + + private UUID getAdminUserId() throws HttpResponseException { + UserResourceTest userResourceTest = new UserResourceTest(); + User adminUser = userResourceTest.getEntityByName("admin", ADMIN_AUTH_HEADERS); + return adminUser.getId(); + } + + protected String initiateExport(String entityName) throws IOException { WebTarget target = getResourceByName(entityName + "/export"); - return TestUtils.get(target, String.class, ADMIN_AUTH_HEADERS); + CSVExportResponse response = + TestUtils.getWithResponse( + target, CSVExportResponse.class, ADMIN_AUTH_HEADERS, Status.ACCEPTED.getStatusCode()); + return response.getJobId(); } @SneakyThrows @@ -4001,7 +4088,7 @@ public abstract class EntityResourceTest { } @Override - protected String exportCsv(String teamName) throws HttpResponseException { + protected String initiateExport(String teamName) throws HttpResponseException { WebTarget target = getCollection().path("/export"); target = target.queryParam("team", teamName); - return TestUtils.get(target, String.class, ADMIN_AUTH_HEADERS); + CSVExportResponse response = + TestUtils.getWithResponse( + target, CSVExportResponse.class, ADMIN_AUTH_HEADERS, Status.ACCEPTED.getStatusCode()); + return response.getJobId(); } } diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/banner/ic-banner-error.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/banner/ic-banner-error.svg new file mode 100644 index 00000000000..01728f05a13 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/banner/ic-banner-error.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/banner/ic-banner-success.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/banner/ic-banner-success.svg new file mode 100644 index 00000000000..55f35d7c54c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/banner/ic-banner-success.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityExportModalProvider/EntityExportModalProvider.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityExportModalProvider/EntityExportModalProvider.component.tsx index d398d3aa0bd..a4b0ae70c71 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityExportModalProvider/EntityExportModalProvider.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityExportModalProvider/EntityExportModalProvider.component.tsx @@ -12,11 +12,15 @@ */ import { Form, Input, Modal } from 'antd'; import { AxiosError } from 'axios'; -import React, { ReactNode, useEffect, useMemo, useState } from 'react'; +import { isString } from 'lodash'; +import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { getCurrentISODate } from '../../../utils/date-time/DateTimeUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; +import Banner, { BannerProps } from '../../common/Banner/Banner'; import { + CSVExportJob, + CSVExportWebsocketResponse, EntityExportModalContextProps, ExportData, } from './EntityExportModalProvider.interface'; @@ -35,6 +39,11 @@ export const EntityExportModalProvider = ({ const { t } = useTranslation(); const [exportData, setExportData] = useState(null); const [downloading, setDownloading] = useState(false); + + const csvExportJobRef = useRef>(); + + const [csvExportJob, setCSVExportJob] = useState>(); + const handleCancel = () => { setExportData(null); }; @@ -70,11 +79,55 @@ export const EntityExportModalProvider = ({ setDownloading(true); const data = await exportData.onExport(exportData.name); - handleDownload(data, fileName); - handleCancel(); + if (isString(data)) { + handleDownload(data, fileName); + handleCancel(); + setDownloading(false); + } else { + const jobData = { + jobId: data.jobId, + fileName: fileName, + message: data.message, + }; + + setCSVExportJob(jobData); + csvExportJobRef.current = jobData; + } } catch (error) { showErrorToast(error as AxiosError); - } finally { + setDownloading(false); + } + }; + + const handleCSVExportSuccess = (data: string, fileName?: string) => { + handleDownload( + data, + fileName ?? `${exportData?.name}_${getCurrentISODate()}` + ); + setDownloading(false); + handleCancel(); + setCSVExportJob(undefined); + csvExportJobRef.current = undefined; + }; + + const handleCSVExportJobUpdate = ( + response: Partial + ) => { + const updatedCSVExportJob: Partial = { + ...response, + ...csvExportJobRef.current, + }; + + setCSVExportJob(updatedCSVExportJob); + + csvExportJobRef.current = updatedCSVExportJob; + + if (response.status === 'COMPLETED' && response.data) { + handleCSVExportSuccess( + response.data ?? '', + csvExportJobRef.current?.fileName + ); + } else { setDownloading(false); } }; @@ -88,7 +141,23 @@ export const EntityExportModalProvider = ({ } }, [exportData]); - const providerValue = useMemo(() => ({ showModal }), []); + const providerValue = useMemo( + () => ({ + showModal, + onUpdateCSVExportJob: handleCSVExportJobUpdate, + }), + [] + ); + + const bannerConfig = useMemo(() => { + const isCompleted = csvExportJob?.status === 'COMPLETED'; + + return { + type: isCompleted ? 'success' : 'error', + message: isCompleted ? csvExportJob?.message : csvExportJob?.error, + hasJobId: !!csvExportJob?.jobId, + }; + }, [csvExportJob]); return ( @@ -124,6 +193,14 @@ export const EntityExportModalProvider = ({ + + {bannerConfig.hasJobId && bannerConfig.message && ( + + )} )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityExportModalProvider/EntityExportModalProvider.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityExportModalProvider/EntityExportModalProvider.interface.ts index da0f95cc683..0bc707b56fe 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityExportModalProvider/EntityExportModalProvider.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityExportModalProvider/EntityExportModalProvider.interface.ts @@ -10,11 +10,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +export type CSVExportResponse = { + jobId: string; + message: string; +}; + +export type CSVExportWebsocketResponse = { + jobId: string; + status: 'COMPLETED' | 'FAILED'; + data: string; + error: string | null; +}; + +export type CSVExportJob = { + fileName: string; +} & Partial & + CSVExportResponse; + export type ExportData = { name: string; title?: string; - onExport: (name: string) => Promise; + onExport: (name: string) => Promise; }; export interface EntityExportModalContextProps { showModal: (data: ExportData) => void; + onUpdateCSVExportJob: (data: Partial) => void; } 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 2d47e976bf1..d5cfffd14b7 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 @@ -90,6 +90,8 @@ import { ActivityFeedTabs } from '../ActivityFeed/ActivityFeedTab/ActivityFeedTa import SearchOptions from '../AppBar/SearchOptions'; import Suggestions from '../AppBar/Suggestions'; import CmdKIcon from '../common/CmdKIcon/CmdKIcon.component'; +import { useEntityExportModalProvider } from '../Entity/EntityExportModalProvider/EntityExportModalProvider.component'; +import { CSVExportWebsocketResponse } from '../Entity/EntityExportModalProvider/EntityExportModalProvider.interface'; import WhatsNewModal from '../Modals/WhatsNewModal/WhatsNewModal'; import NotificationBox from '../NotificationBox/NotificationBox.component'; import { UserProfileIcon } from '../Settings/Users/UserProfileIcon/UserProfileIcon.component'; @@ -110,6 +112,7 @@ const NavBar = ({ handleOnClick, handleClear, }: NavBarProps) => { + const { onUpdateCSVExportJob } = useEntityExportModalProvider(); const { searchCriteria, updateSearchCriteria } = useApplicationStore(); const searchContainerRef = useRef(null); const Logo = useMemo(() => brandImageClassBase.getMonogram().src, []); @@ -349,11 +352,22 @@ const NavBar = ({ ); } }); + + socket.on(SOCKET_EVENTS.CSV_EXPORT_CHANNEL, (exportResponse) => { + if (exportResponse) { + const exportResponseData = JSON.parse( + exportResponse + ) as CSVExportWebsocketResponse; + + onUpdateCSVExportJob(exportResponseData); + } + }); } return () => { socket && socket.off(SOCKET_EVENTS.TASK_CHANNEL); socket && socket.off(SOCKET_EVENTS.MENTION_CHANNEL); + socket && socket.off(SOCKET_EVENTS.CSV_EXPORT_CHANNEL); }; }, [socket]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Banner/Banner.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Banner/Banner.tsx new file mode 100644 index 00000000000..36727e5334d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Banner/Banner.tsx @@ -0,0 +1,22 @@ +import React, { FC } from 'react'; +import './banner.less'; + +import classNames from 'classnames'; +import { ReactComponent as ErrorIcon } from '../../../assets/svg/banner/ic-banner-error.svg'; +import { ReactComponent as SuccessIcon } from '../../../assets/svg/banner/ic-banner-success.svg'; + +export interface BannerProps extends React.HTMLAttributes { + type: 'success' | 'error'; + message: string; +} + +const Banner: FC = ({ type, message, className }) => { + return ( +
+ {type === 'success' ? : } + {message} +
+ ); +}; + +export default Banner; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Banner/banner.less b/openmetadata-ui/src/main/resources/ui/src/components/common/Banner/banner.less new file mode 100644 index 00000000000..78246e4e25e --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Banner/banner.less @@ -0,0 +1,23 @@ +@import (reference) url('../../../styles/variables.less'); + +.message-banner-wrapper { + display: flex; + gap: 8px; + align-items: center; + backdrop-filter: blur(500px); + padding: 8px 16px; + + &.border-radius { + border-radius: 4px; + } + + &.success { + background-color: @success-bg-color; + color: @success-color; + } + + &.error { + background-color: @error-bg-color; + color: @error-color; + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts index 5d716c8eff3..fcdb4da682a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts @@ -295,6 +295,7 @@ export const SOCKET_EVENTS = { TASK_CHANNEL: 'taskChannel', MENTION_CHANNEL: 'mentionChannel', JOB_STATUS: 'jobStatus', + CSV_EXPORT_CHANNEL: 'csvExportChannel', }; export const IN_PAGE_SEARCH_ROUTES: Record> = { 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 e3c5b2d155f..f6479b41f26 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 @@ -773,6 +773,7 @@ "no-description": "Sen descrición", "no-diff-available": "Non hai diferenzas dispoñibles", "no-entity": "Non hai {{entity}}", + "no-entity-available": "No {{entity}} are available", "no-entity-selected": "Non hai {{entity}} seleccionado", "no-matching-data-asset": "Non se atoparon activos de datos coincidentes", "no-of-test": "Nº de probas", diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/glossaryAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/glossaryAPI.ts index e85044b170f..3d1c58db5c7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/glossaryAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/glossaryAPI.ts @@ -14,6 +14,7 @@ import { AxiosResponse } from 'axios'; import { Operation } from 'fast-json-patch'; import { PagingResponse } from 'Models'; +import { CSVExportResponse } from '../components/Entity/EntityExportModalProvider/EntityExportModalProvider.interface'; import { VotingDataProps } from '../components/Entity/Voting/voting.interface'; import { ES_MAX_PAGE_SIZE, PAGE_SIZE_MEDIUM } from '../constants/constants'; import { TabSpecificField } from '../enums/entity.enum'; @@ -172,7 +173,7 @@ export const deleteGlossaryTerm = (id: string) => { }; export const exportGlossaryInCSVFormat = async (glossaryName: string) => { - const response = await APIClient.get( + const response = await APIClient.get( `/glossaries/name/${getEncodedFqn(glossaryName)}/export` ); diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/teamsAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/teamsAPI.ts index e8b363924f1..045747d9cf5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/teamsAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/teamsAPI.ts @@ -14,6 +14,7 @@ import { AxiosResponse } from 'axios'; import { Operation } from 'fast-json-patch'; import { PagingResponse, RestoreRequestType } from 'Models'; +import { CSVExportResponse } from '../components/Entity/EntityExportModalProvider/EntityExportModalProvider.interface'; import { CreateTeam } from '../generated/api/teams/createTeam'; import { Team } from '../generated/entity/teams/team'; import { TeamHierarchy } from '../generated/entity/teams/teamHierarchy'; @@ -90,7 +91,7 @@ export const restoreTeam = async (id: string) => { }; export const exportTeam = async (teamName: string) => { - const response = await APIClient.get( + const response = await APIClient.get( `/teams/name/${getEncodedFqn(teamName)}/export` ); @@ -98,7 +99,7 @@ export const exportTeam = async (teamName: string) => { }; export const exportUserOfTeam = async (team: string) => { - const response = await APIClient.get(`/users/export`, { + const response = await APIClient.get(`/users/export`, { params: { team }, }); diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/variables.less b/openmetadata-ui/src/main/resources/ui/src/styles/variables.less index 05370443569..16954b63af2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/variables.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/variables.less @@ -168,3 +168,8 @@ @margin-sm: 12px; // Form controls and items @margin-xs: 8px; // small items @margin-xss: 4px; // more small + +@error-bg-color: rgb(from @error-color r g b / 0.1); +@success-bg-color: rgb(from @success-color r g b / 0.1); +@warning-bg-color: rgb(from @warning-color r g b / 0.1); +@info-bg-color: rgb(from @info-color r g b / 0.1);