diff --git a/ui/src/api/services/jobService.ts b/ui/src/api/services/jobService.ts index 7b3763d2..832b14b1 100644 --- a/ui/src/api/services/jobService.ts +++ b/ui/src/api/services/jobService.ts @@ -1,7 +1,6 @@ import api from "../axios" import { API_CONFIG } from "../config" import { - APIResponse, Job, JobBase, JobTask, @@ -151,11 +150,23 @@ export const jobService = { } }, - clearDestination: async ( + clearDestination: async (jobId: string): Promise<{ message: string }> => { + try { + const response = await api.post<{ message: string }>( + `${API_CONFIG.ENDPOINTS.JOBS(API_CONFIG.PROJECT_ID)}/${jobId}/clear-destination`, + ) + + return response.data + } catch (error) { + console.error("Error clearing destination:", error) + throw error + } + }, + getClearDestinationStatus: async ( jobId: string, - ): Promise> => { + ): Promise<{ running: boolean }> => { try { - const response = await api.post>( + const response = await api.get<{ running: boolean }>( `${API_CONFIG.ENDPOINTS.JOBS(API_CONFIG.PROJECT_ID)}/${jobId}/clear-destination`, ) @@ -175,7 +186,7 @@ export const jobService = { }>( `${API_CONFIG.ENDPOINTS.JOBS(API_CONFIG.PROJECT_ID)}/${jobId}/stream-difference`, { updated_streams_config: streamsConfig }, - {timeout: 30000} + { timeout: 30000 }, ) return response.data } catch (error) { diff --git a/ui/src/api/services/notificationService.ts b/ui/src/api/services/notificationService.ts index a5f1b081..efa92fdd 100644 --- a/ui/src/api/services/notificationService.ts +++ b/ui/src/api/services/notificationService.ts @@ -3,6 +3,9 @@ import { message } from "antd" /** * A decoupled service for showing UI notifications. */ + +const ERROR_MESSAGE_DURATION = 6 // seconds + export const notificationService = { success: (msg: string) => { if (msg) { @@ -13,7 +16,7 @@ export const notificationService = { error: (msg: string) => { if (msg) { message.destroy() - message.error(msg) + message.error(msg, ERROR_MESSAGE_DURATION) } }, } diff --git a/ui/src/modules/common/Modals/StreamDifferenceModal.tsx b/ui/src/modules/common/Modals/StreamDifferenceModal.tsx index 8d9e7b04..9e53c1e6 100644 --- a/ui/src/modules/common/Modals/StreamDifferenceModal.tsx +++ b/ui/src/modules/common/Modals/StreamDifferenceModal.tsx @@ -2,11 +2,14 @@ import { InfoIcon, WarningIcon } from "@phosphor-icons/react" import { Button, Modal } from "antd" import { useAppStore } from "../../../store" import { StreamDifferenceModalProps } from "../../../types/modalTypes" +import { useState } from "react" const StreamDifferenceModal = ({ streamDifference, onConfirm, }: StreamDifferenceModalProps) => { + const [isLoading, setIsLoading] = useState(false) + const { showStreamDifferenceModal, setShowStreamDifferenceModal } = useAppStore() @@ -14,8 +17,11 @@ const StreamDifferenceModal = ({ setShowStreamDifferenceModal(false) } - const handleFinish = () => { - onConfirm() + const handleFinish = async () => { + setIsLoading(true) + await onConfirm() + setIsLoading(false) + setShowStreamDifferenceModal(false) } @@ -74,6 +80,7 @@ const StreamDifferenceModal = ({ key="edit" type="primary" onClick={handleFinish} + loading={isLoading} className="bg-primary hover:bg-primary-600" > Confirm and Finish diff --git a/ui/src/modules/common/Modals/StreamEditDisabledModal.tsx b/ui/src/modules/common/Modals/StreamEditDisabledModal.tsx index 696a89ec..7fb0801a 100644 --- a/ui/src/modules/common/Modals/StreamEditDisabledModal.tsx +++ b/ui/src/modules/common/Modals/StreamEditDisabledModal.tsx @@ -10,8 +10,8 @@ const StreamEditDisabledModal = ({ from }: StreamEditDisabledModalProps) => { useAppStore() const handleCloseModal = () => { - navigate("/jobs") setShowStreamEditDisabledModal(false) + navigate("/jobs") } return ( diff --git a/ui/src/modules/jobs/components/JobTable.tsx b/ui/src/modules/jobs/components/JobTable.tsx index 4e6b23c4..bd07fdbe 100644 --- a/ui/src/modules/jobs/components/JobTable.tsx +++ b/ui/src/modules/jobs/components/JobTable.tsx @@ -24,6 +24,7 @@ import { } from "../../../utils/utils" import { getStatusIcon } from "../../../utils/statusIcons" import { PAGE_SIZE } from "../../../utils/constants" +import { useAppStore } from "../../../store" const formatLastSyncTime = (text?: string) => { if (!text) return
-
@@ -49,12 +50,14 @@ const JobTable: React.FC = ({ const [searchText, setSearchText] = useState("") const [currentPage, setCurrentPage] = useState(1) const navigate = useNavigate() + const { setSelectedJobId } = useAppStore() const handleViewHistory = (jobId: string) => { navigate(`/jobs/${jobId}/history`) } const handleViewSettings = (jobId: string) => { + setSelectedJobId(jobId) navigate(`/jobs/${jobId}/settings`) } @@ -265,15 +268,18 @@ const JobTable: React.FC = ({ /> - +
+
+
diff --git a/ui/src/modules/jobs/pages/JobEdit.tsx b/ui/src/modules/jobs/pages/JobEdit.tsx index 977f3141..80bc7ce2 100644 --- a/ui/src/modules/jobs/pages/JobEdit.tsx +++ b/ui/src/modules/jobs/pages/JobEdit.tsx @@ -14,7 +14,6 @@ import { SourceData, DestinationData, StreamsDataStructure, - JobType, } from "../../../types" import JobConfiguration from "../components/JobConfiguration" import StepProgress from "../components/StepIndicator" @@ -131,12 +130,13 @@ const JobEdit: React.FC = () => { const { jobId } = useParams<{ jobId: string }>() const { jobs, + selectedJobId, fetchJobs, fetchSources, fetchDestinations, + fetchSelectedClearDestinationStatus, setShowResetStreamsModal, setShowStreamDifferenceModal, - setShowStreamEditDisabledModal, } = useAppStore() const [currentStep, setCurrentStep] = useState( @@ -172,15 +172,9 @@ const JobEdit: React.FC = () => { fetchDestinations() }, []) - // Disable stream editing if clear destination is running useEffect(() => { - if ( - job?.last_run_type === JobType.ClearDestination && - job?.last_run_state.toLowerCase() === "running" - ) { - setShowStreamEditDisabledModal(true) - } - }, [job]) + fetchSelectedClearDestinationStatus() + }, [selectedJobId]) const initializeFromExistingJob = (job: Job) => { setJobName(job.name) diff --git a/ui/src/modules/jobs/pages/JobSettings.tsx b/ui/src/modules/jobs/pages/JobSettings.tsx index e20545cd..7a822a0d 100644 --- a/ui/src/modules/jobs/pages/JobSettings.tsx +++ b/ui/src/modules/jobs/pages/JobSettings.tsx @@ -1,6 +1,15 @@ import { useState, useEffect } from "react" import { useParams, Link, useNavigate } from "react-router-dom" -import { Input, Button, Switch, message, Select, Radio, Tooltip } from "antd" +import { + Input, + Button, + Switch, + message, + Select, + Radio, + Tooltip, + Spin, +} from "antd" import { InfoIcon, ArrowLeftIcon } from "@phosphor-icons/react" import parser from "cron-parser" @@ -17,7 +26,6 @@ import { DAYS, FREQUENCY_OPTIONS } from "../../../utils/constants" import DeleteJobModal from "../../common/Modals/DeleteJobModal" import ClearDataModal from "../../common/Modals/ClearDataModal" import ClearDestinationModal from "../../common/Modals/ClearDestinationModal" -import { JobType } from "../../../types/jobTypes" import StreamEditDisabledModal from "../../common/Modals/StreamEditDisabledModal" const JobSettings: React.FC = () => { @@ -45,12 +53,15 @@ const JobSettings: React.FC = () => { } const { + selectedJobId, jobs, + isClearDestinationStatusLoading, fetchJobs, + fetchSelectedClearDestinationStatus, + selectedClearDestinationRunning, setShowDeleteJobModal, setSelectedJobId, setShowClearDestinationModal, - setShowStreamEditDisabledModal, } = useAppStore() useEffect(() => { @@ -70,9 +81,9 @@ const JobSettings: React.FC = () => { } }, [job]) - const isClearDestinationRunning = - job?.last_run_type === JobType.ClearDestination && - job?.last_run_state.toLowerCase() === "running" + useEffect(() => { + fetchSelectedClearDestinationStatus() + }, [selectedJobId]) const getParsedDate = (value: Date) => value.toUTCString() @@ -219,12 +230,6 @@ const JobSettings: React.FC = () => { setShowClearDestinationModal(true) } - useEffect(() => { - if (isClearDestinationRunning) { - setShowStreamEditDisabledModal(true) - } - }, [isClearDestinationRunning]) - // Helper to determine if time selection should be shown const isTimeSelectionFrequency = (freq: string): boolean => { return freq === "days" || freq === "weeks" @@ -282,6 +287,17 @@ const JobSettings: React.FC = () => { } } + if (isClearDestinationStatusLoading) { + return ( +
+ +
+ ) + } + return ( <>
@@ -467,7 +483,9 @@ const JobSettings: React.FC = () => { diff --git a/ui/src/modules/jobs/pages/SchemaConfiguration.tsx b/ui/src/modules/jobs/pages/SchemaConfiguration.tsx index 0167d2c7..b2df6c66 100644 --- a/ui/src/modules/jobs/pages/SchemaConfiguration.tsx +++ b/ui/src/modules/jobs/pages/SchemaConfiguration.tsx @@ -46,8 +46,12 @@ const SchemaConfiguration: React.FC = ({ onLoadingChange, }) => { const prevSourceConfig = useRef(sourceConfig) - const { setShowDestinationDatabaseModal, ingestionMode, setIngestionMode } = - useAppStore() + const { + isClearDestinationStatusLoading, + setShowDestinationDatabaseModal, + ingestionMode, + setIngestionMode, + } = useAppStore() const [searchText, setSearchText] = useState("") const [selectedFilters, setSelectedFilters] = useState([ "All tables", @@ -62,7 +66,7 @@ const SchemaConfiguration: React.FC = ({ } streams: StreamData[] } | null>(initialStreamsData || null) - const [loading, setLoading] = useState(!initialStreamsData) + const [isStreamsLoading, setIsStreamsLoading] = useState(!initialStreamsData) // Store initial streams data for reference const [initialStreamsState, setInitialStreamsState] = useState(initialStreamsData) @@ -82,6 +86,8 @@ const SchemaConfiguration: React.FC = ({ return !stream?.disabled } + const isLoading = isStreamsLoading || isClearDestinationStatusLoading + // Check if first stream has destination_database and compute values const { destinationDatabase, destinationDatabaseForModal } = useMemo(() => { if (!apiResponse?.streams || apiResponse.streams.length === 0) { @@ -125,7 +131,7 @@ const SchemaConfiguration: React.FC = ({ ) { setApiResponse(initialStreamsData) setSelectedStreams(initialStreamsData) - setLoading(false) + setIsStreamsLoading(false) onLoadingChange?.(false) initialized.current = true @@ -140,7 +146,7 @@ const SchemaConfiguration: React.FC = ({ if (initialized.current) return onLoadingChange?.(true) - setLoading(true) + setIsStreamsLoading(true) try { const response = await sourceService.getSourceStreams( sourceName, @@ -201,7 +207,7 @@ const SchemaConfiguration: React.FC = ({ } catch (error) { console.error("Error fetching source streams:", error) } finally { - setLoading(false) + setIsStreamsLoading(false) onLoadingChange?.(false) } } @@ -656,13 +662,7 @@ const SchemaConfiguration: React.FC = ({
{destinationDatabase && (
-
+
@@ -726,7 +726,7 @@ const SchemaConfiguration: React.FC = ({
- {!loading && apiResponse?.streams ? ( + {!isLoading && apiResponse?.streams ? ( = ({ }} onIngestionModeChange={handleAllIngestionModeChange} /> - ) : loading ? ( + ) : isLoading ? (
@@ -757,7 +757,7 @@ const SchemaConfiguration: React.FC = ({
{activeStreamData ? ( void setSelectedHistoryId: (id: string | null) => void setSelectedSource: (source: Entity) => void setSelectedDestination: (destination: Entity) => void + fetchSelectedClearDestinationStatus: () => void } -export const createSelectionSlice: StateCreator = set => ({ +export const createSelectionSlice: StateCreator< + SelectionSlice & ModalSlice, + [], + [], + SelectionSlice +> = (set, get) => ({ selectedJobId: null, selectedHistoryId: null, selectedSource: {} as Entity, selectedDestination: {} as Entity, + selectedClearDestinationRunning: false, + isClearDestinationStatusLoading: false, setSelectedJobId: id => set({ selectedJobId: id }), setSelectedHistoryId: id => set({ selectedHistoryId: id }), setSelectedSource: source => set({ selectedSource: source }), setSelectedDestination: destination => set({ selectedDestination: destination }), + fetchSelectedClearDestinationStatus: async () => { + const jobId = get().selectedJobId + + if (!jobId) return + + try { + set({ + isClearDestinationStatusLoading: true, + }) + const status = await jobService.getClearDestinationStatus(jobId) + + if (status.running) { + set({ showStreamEditDisabledModal: true }) + } + + set({ + selectedClearDestinationRunning: status.running, + isClearDestinationStatusLoading: false, + }) + } catch (error) { + console.error("Error fetching clear destination status:", error) + set({ + isClearDestinationStatusLoading: false, + }) + } + }, })