From ed12fff994483919ce62b0b6dc5d6e53089d4e01 Mon Sep 17 00:00:00 2001 From: Sam Chau Date: Sat, 7 Sep 2024 17:17:25 +0930 Subject: [PATCH] chore: add watchdog support for backend HMR --- backend/Dockerfile | 6 +- backend/app/format.py | 58 +- backend/requirements.txt | 3 +- backend/run.py | 2 +- backend/run_dev.py | 49 + docker-compose.yml | 39 +- .../src/components/settings/SettingsPage.jsx | 1514 +++++++++-------- 7 files changed, 915 insertions(+), 756 deletions(-) create mode 100644 backend/run_dev.py diff --git a/backend/Dockerfile b/backend/Dockerfile index fc51b20..7d0ba6d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,10 +1,6 @@ FROM python:3.9 - WORKDIR /app - COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt - COPY . . - -CMD ["python", "run.py"] \ No newline at end of file +CMD ["python", "run_dev.py"] \ No newline at end of file diff --git a/backend/app/format.py b/backend/app/format.py index 6efad9a..8d1d126 100644 --- a/backend/app/format.py +++ b/backend/app/format.py @@ -16,6 +16,7 @@ os.makedirs(FORMAT_DIR, exist_ok=True) logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) + @bp.route('', methods=['GET', 'POST']) def handle_formats(): if request.method == 'POST': @@ -26,6 +27,7 @@ def handle_formats(): formats = load_all_formats() return jsonify(formats) + @bp.route('/', methods=['GET', 'PUT', 'DELETE']) def handle_format(id): if request.method == 'GET': @@ -46,14 +48,17 @@ def handle_format(id): return jsonify(result), 400 return jsonify(result), 200 + def is_format_used_in_profile(format_id): profiles = load_all_profiles() for profile in profiles: for custom_format in profile.get('custom_formats', []): - if custom_format.get('id') == format_id and custom_format.get('score', 0) != 0: + if custom_format.get('id') == format_id and custom_format.get( + 'score', 0) != 0: return True return False + def save_format(data): logger.info("Received data for saving format: %s", data) @@ -71,9 +76,11 @@ def save_format(data): existing_filename = os.path.join(FORMAT_DIR, f"{format_id}.yml") if os.path.exists(existing_filename): existing_data = load_format(format_id) - date_created = existing_data.get('date_created', get_current_timestamp()) + date_created = existing_data.get('date_created', + get_current_timestamp()) else: - raise FileNotFoundError(f"No existing file found for ID: {format_id}") + raise FileNotFoundError( + f"No existing file found for ID: {format_id}") date_modified = get_current_timestamp() @@ -81,12 +88,11 @@ def save_format(data): conditions = [] for condition in data.get('conditions', []): logger.info("Processing condition: %s", condition) - cond_dict = OrderedDict([ - ('type', condition['type']), - ('name', sanitize_input(condition['name'])), - ('negate', condition.get('negate', False)), - ('required', condition.get('required', False)) - ]) + cond_dict = OrderedDict([('type', condition['type']), + ('name', sanitize_input(condition['name'])), + ('negate', condition.get('negate', False)), + ('required', condition.get('required', + False))]) if condition['type'] == 'regex': cond_dict['regex_id'] = condition['regex_id'] elif condition['type'] == 'size': @@ -100,25 +106,25 @@ def save_format(data): tags = [sanitize_input(tag) for tag in data.get('tags', [])] # Construct the ordered data - ordered_data = OrderedDict([ - ('id', format_id), - ('name', name), - ('description', description), - ('date_created', str(date_created)), - ('date_modified', str(date_modified)), - ('conditions', conditions), - ('tags', tags) - ]) + ordered_data = OrderedDict([('id', format_id), ('name', name), + ('description', description), + ('date_created', str(date_created)), + ('date_modified', str(date_modified)), + ('conditions', conditions), ('tags', tags)]) # Generate the filename using only the ID filename = os.path.join(FORMAT_DIR, f"{format_id}.yml") - + # Write to the file with open(filename, 'w') as file: - yaml.dump(ordered_data, file, default_flow_style=False, Dumper=yaml.SafeDumper) - + yaml.dump(ordered_data, + file, + default_flow_style=False, + Dumper=yaml.SafeDumper) + return ordered_data + def load_format(id): filename = os.path.join(FORMAT_DIR, f"{id}.yml") if os.path.exists(filename): @@ -127,12 +133,16 @@ def load_format(id): return data return None + def delete_format(id): if is_format_used_in_profile(id): - return {"error": "Format in use", "message": "This format is being used in one or more profiles."} - + return { + "error": "Format in use", + "message": "This format is being used in one or more profiles." + } + filename = os.path.join(FORMAT_DIR, f"{id}.yml") if os.path.exists(filename): os.remove(filename) return {"message": f"Format with ID {id} deleted."} - return {"error": f"Format with ID {id} not found."} \ No newline at end of file + return {"error": f"Format with ID {id} not found."} diff --git a/backend/requirements.txt b/backend/requirements.txt index 1555ae3..3c1a64d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,4 +3,5 @@ Flask-CORS==3.0.10 PyYAML==5.4.1 requests==2.26.0 Werkzeug==2.0.1 -GitPython==3.1.24 \ No newline at end of file +GitPython==3.1.24 +watchdog \ No newline at end of file diff --git a/backend/run.py b/backend/run.py index d1374de..1cb3148 100644 --- a/backend/run.py +++ b/backend/run.py @@ -2,4 +2,4 @@ from app import create_app if __name__ == '__main__': app = create_app() - app.run(debug=True, host='0.0.0.0') \ No newline at end of file + app.run(debug=True, host='0.0.0.0') diff --git a/backend/run_dev.py b/backend/run_dev.py new file mode 100644 index 0000000..ad3b58e --- /dev/null +++ b/backend/run_dev.py @@ -0,0 +1,49 @@ +import sys +import time +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler +import subprocess +import os + + +class Reloader(FileSystemEventHandler): + + def __init__(self): + self.process = None + self.last_restart = 0 + self.start_app() + + def on_any_event(self, event): + if event.src_path.endswith( + '.py') and not event.src_path.endswith('run_dev.py'): + current_time = time.time() + if current_time - self.last_restart > 1: # Prevent rapid restarts + print(f"Detected change in {event.src_path}, restarting...") + self.restart_app() + self.last_restart = current_time + + def start_app(self): + env = os.environ.copy() + env['FLASK_ENV'] = 'development' + self.process = subprocess.Popen([sys.executable, 'run.py'], env=env) + + def restart_app(self): + if self.process: + self.process.terminate() + self.process.wait() + self.start_app() + + +if __name__ == "__main__": + path = '.' + event_handler = Reloader() + observer = Observer() + observer.schedule(event_handler, path, recursive=True) + observer.start() + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + observer.stop() + observer.join() diff --git a/docker-compose.yml b/docker-compose.yml index 3c76e11..9034625 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,23 +1,24 @@ version: '3.8' services: - frontend: - build: ./frontend - ports: - - "3000:3000" - volumes: - - ./frontend:/app - - /app/node_modules - environment: - - VITE_API_URL=http://192.168.1.111:5000 # Replace with your host machine's IP - - CHOKIDAR_USEPOLLING=true - - backend: - build: ./backend - ports: - - "5000:5000" - volumes: - - ./backend:/app - - backend_data:/app/data + frontend: + build: ./frontend + ports: + - '3000:3000' + volumes: + - ./frontend:/app + - /app/node_modules + environment: + - VITE_API_URL=http://192.168.1.111:5000 # Replace with your host machine's IP + - CHOKIDAR_USEPOLLING=true + backend: + build: ./backend + ports: + - '5000:5000' + volumes: + - ./backend:/app + - backend_data:/app/data + environment: + - FLASK_ENV=development volumes: - backend_data: \ No newline at end of file + backend_data: diff --git a/frontend/src/components/settings/SettingsPage.jsx b/frontend/src/components/settings/SettingsPage.jsx index 9184934..3a9535c 100644 --- a/frontend/src/components/settings/SettingsPage.jsx +++ b/frontend/src/components/settings/SettingsPage.jsx @@ -1,726 +1,828 @@ -import React, { useState, useEffect } from "react"; +import React, {useState, useEffect} from 'react'; import { - getSettings, - getGitStatus, - addFiles, - pushFiles, - revertFile, - pullBranch, - getDiff, - unlinkRepo, -} from "../../api/api"; -import ApiKeyModal from "./ApiKeyModal"; -import UnlinkModal from "./UnlinkModal"; -import SettingsBranchModal from "./SettingsBranchModal"; + getSettings, + getGitStatus, + addFiles, + pushFiles, + revertFile, + pullBranch, + getDiff, + unlinkRepo +} from '../../api/api'; +import ApiKeyModal from './ApiKeyModal'; +import UnlinkModal from './UnlinkModal'; +import SettingsBranchModal from './SettingsBranchModal'; import { - FileText, - Code, - AlertCircle, - Plus, - MinusCircle, - Edit, - GitBranch, - Loader, - Eye, - RotateCcw, - Download, - ArrowDown, - ArrowUp, - CheckCircle, - File, - Settings, - Unlink, -} from "lucide-react"; -import Alert from "../ui/Alert"; -import CommitSection from "./CommitSection"; -import Tooltip from "../ui/Tooltip"; -import DiffModal from "./DiffModal"; + FileText, + Code, + AlertCircle, + Plus, + MinusCircle, + Edit, + GitBranch, + Loader, + Eye, + RotateCcw, + Download, + ArrowDown, + ArrowUp, + CheckCircle, + File, + Settings, + Unlink +} from 'lucide-react'; +import Alert from '../ui/Alert'; +import CommitSection from './CommitSection'; +import Tooltip from '../ui/Tooltip'; +import DiffModal from './DiffModal'; const SettingsPage = () => { - const [settings, setSettings] = useState(null); - const [status, setStatus] = useState(null); - const [showModal, setShowModal] = useState(false); - const [showBranchModal, setShowBranchModal] = useState(false); - const [loadingAction, setLoadingAction] = useState(""); - const [loadingStatus, setLoadingStatus] = useState(true); - const [commitMessage, setCommitMessage] = useState(""); - const [showUnlinkModal, setShowUnlinkModal] = useState(false); - const [selectedIncomingChanges, setSelectedIncomingChanges] = useState([]); - const [selectedOutgoingChanges, setSelectedOutgoingChanges] = useState([]); - const [showDiffModal, setShowDiffModal] = useState(false); - const [diffContent, setDiffContent] = useState(""); - const [currentChange, setCurrentChange] = useState(null); - const [loadingDiff, setLoadingDiff] = useState(false); - const [selectionType, setSelectionType] = useState(null); - const [sortConfig, setSortConfig] = useState({ - key: "type", - direction: "descending", - }); - - useEffect(() => { - fetchSettings(); - }, []); - - const fetchSettings = async () => { - try { - const fetchedSettings = await getSettings(); - setSettings(fetchedSettings); - if (fetchedSettings) { - await fetchGitStatus(); - } - } catch (error) { - console.error("Error fetching settings:", error); - } - }; - - const sortedChanges = (changes) => { - if (!sortConfig.key) return changes; - - return [...changes].sort((a, b) => { - if (a[sortConfig.key] < b[sortConfig.key]) { - return sortConfig.direction === "ascending" ? -1 : 1; - } - if (a[sortConfig.key] > b[sortConfig.key]) { - return sortConfig.direction === "ascending" ? 1 : -1; - } - return 0; + const [settings, setSettings] = useState(null); + const [status, setStatus] = useState(null); + const [showModal, setShowModal] = useState(false); + const [showBranchModal, setShowBranchModal] = useState(false); + const [loadingAction, setLoadingAction] = useState(''); + const [loadingStatus, setLoadingStatus] = useState(true); + const [commitMessage, setCommitMessage] = useState(''); + const [showUnlinkModal, setShowUnlinkModal] = useState(false); + const [selectedIncomingChanges, setSelectedIncomingChanges] = useState([]); + const [selectedOutgoingChanges, setSelectedOutgoingChanges] = useState([]); + const [showDiffModal, setShowDiffModal] = useState(false); + const [diffContent, setDiffContent] = useState(''); + const [currentChange, setCurrentChange] = useState(null); + const [loadingDiff, setLoadingDiff] = useState(false); + const [selectionType, setSelectionType] = useState(null); + const [sortConfig, setSortConfig] = useState({ + key: 'type', + direction: 'descending' }); - }; - const requestSort = (key) => { - let direction = "ascending"; - if (sortConfig.key === key && sortConfig.direction === "ascending") { - direction = "descending"; - } - setSortConfig({ key, direction }); - }; + useEffect(() => { + fetchSettings(); + }, []); - const SortableHeader = ({ children, sortKey }) => { - const isSorted = sortConfig.key === sortKey; - return ( - requestSort(sortKey)} - > -
- {children} - {isSorted && - (sortConfig.direction === "ascending" ? ( - - ) : ( - - ))} -
- - ); - }; + const fetchSettings = async () => { + try { + const fetchedSettings = await getSettings(); + setSettings(fetchedSettings); + if (fetchedSettings) { + await fetchGitStatus(); + } + } catch (error) { + console.error('Error fetching settings:', error); + } + }; - const fetchGitStatus = async () => { - setLoadingStatus(true); - try { - const result = await getGitStatus(); - console.log("================ Git Status Response ================"); - console.log(JSON.stringify(result, null, 2)); - console.log("======================================================"); + const sortedChanges = changes => { + if (!sortConfig.key) return changes; - if (result.success) { - setStatus({ - ...result.data, - outgoing_changes: Array.isArray(result.data.outgoing_changes) - ? result.data.outgoing_changes - : [], - incoming_changes: Array.isArray(result.data.incoming_changes) - ? result.data.incoming_changes - : [], + return [...changes].sort((a, b) => { + if (a[sortConfig.key] < b[sortConfig.key]) { + return sortConfig.direction === 'ascending' ? -1 : 1; + } + if (a[sortConfig.key] > b[sortConfig.key]) { + return sortConfig.direction === 'ascending' ? 1 : -1; + } + return 0; }); - } - } catch (error) { - console.error("Error fetching Git status:", error); - Alert.error("Failed to fetch Git status"); - } finally { - setLoadingStatus(false); - } - }; + }; - const renderChangeTable = (changes, title, icon, isIncoming) => ( -
-

- {icon} - - {title} ({changes.length}) - -

-
- - - - Status - Type - Name - - - - - - {sortedChanges(changes).map((change, index) => ( - handleSelectChange(change.file_path, isIncoming)} - > - - - - - - - ))} - -
- Actions - - Select -
-
- {getStatusIcon(change.status)} - - {change.staged - ? `${change.status} (Staged)` - : change.status} - -
-
-
- {getTypeIcon(change.type)} - {change.type} -
-
- {change.name || "Unnamed"} - - - - - - e.stopPropagation()} - disabled={!isIncoming && change.staged} - /> -
-
-
- ); - - const getStageButtonTooltip = () => { - if (selectionType === "staged") { - return "These files are already staged"; - } - if (selectedOutgoingChanges.length === 0) { - return "Select files to stage"; - } - return "Stage selected files"; - }; - - const getCommitButtonTooltip = () => { - if (selectionType === "unstaged") { - return "You can only commit staged files"; - } - if (selectedOutgoingChanges.length === 0) { - return "Select files to commit"; - } - if (!commitMessage.trim()) { - return "Enter a commit message"; - } - return "Commit selected files"; - }; - - const getRevertButtonTooltip = () => { - if (selectedOutgoingChanges.length === 0) { - return "Select files to revert"; - } - return "Revert selected files"; - }; - - const handleViewDiff = async (change) => { - setLoadingDiff(true); - try { - const response = await getDiff(change.file_path); - console.log("Diff response:", response); // Add this line to log the response - if (response.success) { - console.log("Diff content:", response.diff); // Add this line to log the diff content - setDiffContent(response.diff); - setCurrentChange(change); - setShowDiffModal(true); - } else { - Alert.error(response.error); - } - } catch (error) { - Alert.error("An unexpected error occurred while fetching the diff."); - console.error("Error fetching diff:", error); - } finally { - setLoadingDiff(false); - setLoadingAction(""); - } - }; - - const handleStageSelectedChanges = async () => { - if (selectedOutgoingChanges.length === 0) { - Alert.warning("Please select at least one change to stage."); - return; - } - - setLoadingAction("stage_selected"); - try { - const response = await addFiles(selectedOutgoingChanges); - if (response.success) { - await fetchGitStatus(); - setSelectedOutgoingChanges([]); // Clear the selected changes after staging - Alert.success(response.message); - } else { - Alert.error(response.error); - } - } catch (error) { - Alert.error("An unexpected error occurred while staging changes."); - console.error("Error staging changes:", error); - } finally { - setLoadingAction(""); - } - }; - - const handleCommitSelectedChanges = async () => { - if (selectedOutgoingChanges.length === 0) { - Alert.warning("Please select at least one change to commit."); - return; - } - - if (!commitMessage.trim()) { - Alert.warning("Please enter a commit message."); - return; - } - - setLoadingAction("commit_selected"); - try { - const response = await pushFiles(selectedOutgoingChanges, commitMessage); - if (response.success) { - await fetchGitStatus(); - setSelectedOutgoingChanges([]); // Clear the selected changes after committing - setCommitMessage(""); - Alert.success(response.message); - } else { - Alert.error(response.error); - } - } catch (error) { - Alert.error("An unexpected error occurred while committing changes."); - console.error("Error committing changes:", error); - } finally { - setLoadingAction(""); - } - }; - - const handleRevertSelectedChanges = async () => { - if (selectedOutgoingChanges.length === 0) { - Alert.warning("Please select at least one change to revert."); - return; - } - - setLoadingAction("revert_selected"); - try { - const response = await Promise.all( - selectedOutgoingChanges.map((filePath) => revertFile(filePath)) - ); - const allSuccessful = response.every((res) => res.success); - if (allSuccessful) { - await fetchGitStatus(); - setSelectedOutgoingChanges([]); // Clear the selected changes after reverting - Alert.success("Selected changes have been reverted successfully."); - } else { - Alert.error("Some changes could not be reverted. Please try again."); - } - } catch (error) { - Alert.error("An unexpected error occurred while reverting changes."); - console.error("Error reverting changes:", error); - } finally { - setLoadingAction(""); - } - }; - - const handlePullSelectedChanges = async () => { - if (selectedIncomingChanges.length === 0) { - Alert.warning("Please select at least one change to pull."); - return; - } - - setLoadingAction("pull_changes"); - try { - // You would need to update your backend to handle pulling specific files - const response = await pullBranch(status.branch, selectedIncomingChanges); - if (response.success) { - await fetchGitStatus(); - setSelectedIncomingChanges([]); // Clear the selected changes after pulling - Alert.success(response.message); - } else { - Alert.error(response.error); - } - } catch (error) { - Alert.error("An unexpected error occurred while pulling changes."); - console.error("Error pulling changes:", error); - } finally { - setLoadingAction(""); - } - }; - - const handleSelectChange = (filePath, isIncoming) => { - if (isIncoming) { - setSelectedIncomingChanges((prevSelected) => { - if (prevSelected.includes(filePath)) { - return prevSelected.filter((path) => path !== filePath); - } else { - return [...prevSelected, filePath]; + const requestSort = key => { + let direction = 'ascending'; + if (sortConfig.key === key && sortConfig.direction === 'ascending') { + direction = 'descending'; } - }); - } else { - const change = status.outgoing_changes.find( - (c) => c.file_path === filePath - ); - const isStaged = change.staged; + setSortConfig({key, direction}); + }; - setSelectedOutgoingChanges((prevSelected) => { - if (prevSelected.includes(filePath)) { - const newSelection = prevSelected.filter((path) => path !== filePath); - if (newSelection.length === 0) setSelectionType(null); - return newSelection; - } else { - if ( - prevSelected.length === 0 || - (isStaged && selectionType === "staged") || - (!isStaged && selectionType === "unstaged") - ) { - setSelectionType(isStaged ? "staged" : "unstaged"); - return [...prevSelected, filePath]; - } else { - return prevSelected; - } + const SortableHeader = ({children, sortKey}) => { + const isSorted = sortConfig.key === sortKey; + return ( + requestSort(sortKey)}> +
+ {children} + {isSorted && + (sortConfig.direction === 'ascending' ? ( + + ) : ( + + ))} +
+ + ); + }; + + const fetchGitStatus = async () => { + setLoadingStatus(true); + try { + const result = await getGitStatus(); + console.log( + '================ Git Status Response ================' + ); + console.log(JSON.stringify(result, null, 2)); + console.log( + '======================================================' + ); + + if (result.success) { + setStatus({ + ...result.data, + outgoing_changes: Array.isArray( + result.data.outgoing_changes + ) + ? result.data.outgoing_changes + : [], + incoming_changes: Array.isArray( + result.data.incoming_changes + ) + ? result.data.incoming_changes + : [] + }); + } + } catch (error) { + console.error('Error fetching Git status:', error); + Alert.error('Failed to fetch Git status'); + } finally { + setLoadingStatus(false); } - }); - } - }; + }; - const loadingMessages = [ - "Checking for changes... don't blink!", - "Syncing with the mothership...", - "Peeking under the hood...", - "Counting bits and bytes...", - "Scanning for modifications...", - "Looking for new stuff...", - "Comparing local and remote...", - "Checking your project's pulse...", - "Analyzing your code's mood...", - "Reading the project's diary...", - ]; - - const getRandomLoadingMessage = () => { - return loadingMessages[Math.floor(Math.random() * loadingMessages.length)]; - }; - - const getStatusIcon = (status) => { - switch (status) { - case "Untracked": - return ; - case "Staged (New)": - return ; - case "Staged (Modified)": - case "Modified": - return ; - case "Deleted": - return ; - case "Deleted (Staged)": - return ; - case "Renamed": - return ; - default: - return ; - } - }; - - const getTypeIcon = (type) => { - switch (type) { - case "Regex Pattern": - return ; - case "Custom Format": - return ; - case "Quality Profile": - return ; - default: - return ; - } - }; - - const handleLinkRepo = async () => { - setLoadingAction(""); - setShowModal(false); - await fetchSettings(); - }; - - - const handleUnlinkRepo = async (removeFiles) => { - setLoadingAction("unlink_repo"); - try { - const response = await unlinkRepo(removeFiles); - if (response.success) { - setSettings(null); - setStatus(null); - Alert.success("Repository unlinked successfully"); - setShowUnlinkModal(false); // Close the modal after unlinking - } else { - Alert.error(response.error || "Failed to unlink repository"); - } - } catch (error) { - Alert.error("An unexpected error occurred while unlinking the repository"); - console.error("Error unlinking repository:", error); - } finally { - setLoadingAction(""); - } -}; - - return ( -
-

- Git Repository Settings -

- {!settings && ( - - )} - {settings && ( -
-
-

- Connected Repository -

-
- - {settings.gitRepo} - - - - -
-
- -
-

- Git Status -

- {loadingStatus ? ( -
- - - {getRandomLoadingMessage()} + const renderChangeTable = (changes, title, icon, isIncoming) => ( +
+

+ {icon} + + {title} ({changes.length}) -

- ) : ( - status && ( - <> -
-
- - - Current Branch: {status.branch} - -
- -
- - {status.incoming_changes.length > 0 && - renderChangeTable( - status.incoming_changes, - "Incoming Changes", - , - true - )} - {status.outgoing_changes.length > 0 && - renderChangeTable( - status.outgoing_changes, - "Outgoing Changes", - , - false - )} - - 0} - /> - - {/* Buttons Below Commit Section */} -
- {/* Conditionally render Stage button */} - {selectedOutgoingChanges.length > 0 && - selectionType !== "staged" && ( - - - - )} - - {/* Conditionally render Commit button */} - {selectedOutgoingChanges.length > 0 && - commitMessage.trim() && - selectionType !== "unstaged" && ( - - - - )} - - {/* Conditionally render Revert button */} - {selectedOutgoingChanges.length > 0 && ( - - - - )} - {/* Conditionally render Pull button */} - {selectedIncomingChanges.length > 0 && ( - - - - )} - -
- - ) - )} -
+ +
+ + + + + Status + + Type + Name + + + + + + {sortedChanges(changes).map((change, index) => ( + + handleSelectChange( + change.file_path, + isIncoming + ) + }> + + + + + + + ))} + +
+ Actions + + Select +
+
+ {getStatusIcon(change.status)} + + {change.staged + ? `${change.status} (Staged)` + : change.status} + +
+
+
+ {getTypeIcon(change.type)} + + {change.type} + +
+
+ {change.name || 'Unnamed'} + + + + + + e.stopPropagation()} + disabled={!isIncoming && change.staged} + /> +
+
- )} - {settings && status && ( - setShowBranchModal(false)} - repoUrl={settings.gitRepo} - currentBranch={status.branch} - onBranchChange={fetchGitStatus} - /> - )} - {showDiffModal && currentChange && ( - setShowDiffModal(false)} - diffContent={diffContent} - type={currentChange.type} - name={currentChange.name} - commitMessage={currentChange.commit_message} - /> - )} - setShowModal(false)} - onSubmit={handleLinkRepo} - /> - setShowUnlinkModal(false)} - onSubmit={handleUnlinkRepo} - /> -
- ); + ); + + const getStageButtonTooltip = () => { + if (selectionType === 'staged') { + return 'These files are already staged'; + } + if (selectedOutgoingChanges.length === 0) { + return 'Select files to stage'; + } + return 'Stage selected files'; + }; + + const getCommitButtonTooltip = () => { + if (selectionType === 'unstaged') { + return 'You can only commit staged files'; + } + if (selectedOutgoingChanges.length === 0) { + return 'Select files to commit'; + } + if (!commitMessage.trim()) { + return 'Enter a commit message'; + } + return 'Commit selected files'; + }; + + const getRevertButtonTooltip = () => { + if (selectedOutgoingChanges.length === 0) { + return 'Select files to revert'; + } + return 'Revert selected files'; + }; + + const handleViewDiff = async change => { + setLoadingDiff(true); + try { + const response = await getDiff(change.file_path); + console.log('Diff response:', response); // Add this line to log the response + if (response.success) { + console.log('Diff content:', response.diff); // Add this line to log the diff content + setDiffContent(response.diff); + setCurrentChange(change); + setShowDiffModal(true); + } else { + Alert.error(response.error); + } + } catch (error) { + Alert.error( + 'An unexpected error occurred while fetching the diff.' + ); + console.error('Error fetching diff:', error); + } finally { + setLoadingDiff(false); + setLoadingAction(''); + } + }; + + const handleStageSelectedChanges = async () => { + if (selectedOutgoingChanges.length === 0) { + Alert.warning('Please select at least one change to stage.'); + return; + } + + setLoadingAction('stage_selected'); + try { + const response = await addFiles(selectedOutgoingChanges); + if (response.success) { + await fetchGitStatus(); + setSelectedOutgoingChanges([]); // Clear the selected changes after staging + Alert.success(response.message); + } else { + Alert.error(response.error); + } + } catch (error) { + Alert.error('An unexpected error occurred while staging changes.'); + console.error('Error staging changes:', error); + } finally { + setLoadingAction(''); + } + }; + + const handleCommitSelectedChanges = async () => { + if (selectedOutgoingChanges.length === 0) { + Alert.warning('Please select at least one change to commit.'); + return; + } + + if (!commitMessage.trim()) { + Alert.warning('Please enter a commit message.'); + return; + } + + setLoadingAction('commit_selected'); + try { + const response = await pushFiles( + selectedOutgoingChanges, + commitMessage + ); + if (response.success) { + await fetchGitStatus(); + setSelectedOutgoingChanges([]); // Clear the selected changes after committing + setCommitMessage(''); + Alert.success(response.message); + } else { + Alert.error(response.error); + } + } catch (error) { + Alert.error( + 'An unexpected error occurred while committing changes.' + ); + console.error('Error committing changes:', error); + } finally { + setLoadingAction(''); + } + }; + + const handleRevertSelectedChanges = async () => { + if (selectedOutgoingChanges.length === 0) { + Alert.warning('Please select at least one change to revert.'); + return; + } + + setLoadingAction('revert_selected'); + try { + const response = await Promise.all( + selectedOutgoingChanges.map(filePath => revertFile(filePath)) + ); + const allSuccessful = response.every(res => res.success); + if (allSuccessful) { + await fetchGitStatus(); + setSelectedOutgoingChanges([]); // Clear the selected changes after reverting + Alert.success( + 'Selected changes have been reverted successfully.' + ); + } else { + Alert.error( + 'Some changes could not be reverted. Please try again.' + ); + } + } catch (error) { + Alert.error( + 'An unexpected error occurred while reverting changes.' + ); + console.error('Error reverting changes:', error); + } finally { + setLoadingAction(''); + } + }; + + const handlePullSelectedChanges = async () => { + if (selectedIncomingChanges.length === 0) { + Alert.warning('Please select at least one change to pull.'); + return; + } + + setLoadingAction('pull_changes'); + try { + // You would need to update your backend to handle pulling specific files + const response = await pullBranch( + status.branch, + selectedIncomingChanges + ); + if (response.success) { + await fetchGitStatus(); + setSelectedIncomingChanges([]); // Clear the selected changes after pulling + Alert.success(response.message); + } else { + Alert.error(response.error); + } + } catch (error) { + Alert.error('An unexpected error occurred while pulling changes.'); + console.error('Error pulling changes:', error); + } finally { + setLoadingAction(''); + } + }; + + const handleSelectChange = (filePath, isIncoming) => { + if (isIncoming) { + setSelectedIncomingChanges(prevSelected => { + if (prevSelected.includes(filePath)) { + return prevSelected.filter(path => path !== filePath); + } else { + return [...prevSelected, filePath]; + } + }); + } else { + const change = status.outgoing_changes.find( + c => c.file_path === filePath + ); + const isStaged = change.staged; + + setSelectedOutgoingChanges(prevSelected => { + if (prevSelected.includes(filePath)) { + const newSelection = prevSelected.filter( + path => path !== filePath + ); + if (newSelection.length === 0) setSelectionType(null); + return newSelection; + } else { + if ( + prevSelected.length === 0 || + (isStaged && selectionType === 'staged') || + (!isStaged && selectionType === 'unstaged') + ) { + setSelectionType(isStaged ? 'staged' : 'unstaged'); + return [...prevSelected, filePath]; + } else { + return prevSelected; + } + } + }); + } + }; + + const loadingMessages = [ + "Checking for changes... don't blink!", + 'Syncing with the mothership...', + 'Peeking under the hood...', + 'Counting bits and bytes...', + 'Scanning for modifications...', + 'Looking for new stuff...', + 'Comparing local and remote...', + "Checking your project's pulse...", + "Analyzing your code's mood...", + "Reading the project's diary..." + ]; + + const getRandomLoadingMessage = () => { + return loadingMessages[ + Math.floor(Math.random() * loadingMessages.length) + ]; + }; + + const getStatusIcon = status => { + switch (status) { + case 'Untracked': + return ; + case 'Staged (New)': + return ; + case 'Staged (Modified)': + case 'Modified': + return ; + case 'Deleted': + return ; + case 'Deleted (Staged)': + return ; + case 'Renamed': + return ; + default: + return ; + } + }; + + const getTypeIcon = type => { + switch (type) { + case 'Regex Pattern': + return ; + case 'Custom Format': + return ; + case 'Quality Profile': + return ; + default: + return ; + } + }; + + const handleLinkRepo = async () => { + setLoadingAction(''); + setShowModal(false); + await fetchSettings(); + }; + + const handleUnlinkRepo = async removeFiles => { + setLoadingAction('unlink_repo'); + try { + const response = await unlinkRepo(removeFiles); + if (response.success) { + setSettings(null); + setStatus(null); + Alert.success('Repository unlinked successfully'); + setShowUnlinkModal(false); // Close the modal after unlinking + } else { + Alert.error(response.error || 'Failed to unlink repository'); + } + } catch (error) { + Alert.error( + 'An unexpected error occurred while unlinking the repository' + ); + console.error('Error unlinking repository:', error); + } finally { + setLoadingAction(''); + } + }; + + return ( +
+

+ Git Repository Settings +

+ {!settings && ( + + )} + {settings && ( +
+
+

+ Connected Repository +

+
+ + {settings.gitRepo} + + + + +
+
+ +
+

+ Git Status +

+ {loadingStatus ? ( +
+ + + {getRandomLoadingMessage()} + +
+ ) : ( + status && ( + <> +
+
+ + + Current Branch: {status.branch} + +
+ +
+ + {status.incoming_changes.length > 0 && + renderChangeTable( + status.incoming_changes, + 'Incoming Changes', + , + true + )} + {status.outgoing_changes.length > 0 && + renderChangeTable( + status.outgoing_changes, + 'Outgoing Changes', + , + false + )} + + 0 + } + /> + + {/* Buttons Below Commit Section */} +
+ {/* Conditionally render Stage button */} + {selectedOutgoingChanges.length > 0 && + selectionType !== 'staged' && ( + + + + )} + + {/* Conditionally render Commit button */} + {selectedOutgoingChanges.length > 0 && + commitMessage.trim() && + selectionType !== 'unstaged' && ( + + + + )} + + {/* Conditionally render Revert button */} + {selectedOutgoingChanges.length > 0 && ( + + + + )} + {/* Conditionally render Pull button */} + {selectedIncomingChanges.length > 0 && ( + + + + )} +
+ + ) + )} +
+
+ )} + {settings && status && ( + setShowBranchModal(false)} + repoUrl={settings.gitRepo} + currentBranch={status.branch} + onBranchChange={fetchGitStatus} + /> + )} + {showDiffModal && currentChange && ( + setShowDiffModal(false)} + diffContent={diffContent} + type={currentChange.type} + name={currentChange.name} + commitMessage={currentChange.commit_message} + /> + )} + setShowModal(false)} + onSubmit={handleLinkRepo} + /> + setShowUnlinkModal(false)} + onSubmit={handleUnlinkRepo} + /> +
+ ); }; export default SettingsPage;