diff --git a/src-tauri/src/commands/container.rs b/src-tauri/src/commands/container.rs index 5e589e7..292a578 100644 --- a/src-tauri/src/commands/container.rs +++ b/src-tauri/src/commands/container.rs @@ -1,6 +1,8 @@ use crate::state::AppState; use crate::utils::terminal::{get_terminal, open_terminal}; -use bollard::container::{ListContainersOptions, LogsOptions, StatsOptions}; +use bollard::container::{ + ListContainersOptions, LogsOptions, RenameContainerOptions, StatsOptions, +}; use bollard::models::{ContainerInspectResponse, ContainerSummary}; use futures_util::StreamExt; use std::collections::HashMap; @@ -192,3 +194,23 @@ pub async fn container_stats( Ok(()) } + +#[tauri::command] +pub async fn rename_container( + state: tauri::State<'_, AppState>, + name: String, + new_name: String, +) -> Result { + let opts = RenameContainerOptions { name: &new_name }; + state + .docker + .rename_container(&name, opts) + .await + .map(|_| { + format!( + "Container '{}' successfully renamed to '{}'", + name, new_name + ) + }) + .map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index bd046f3..0e7b344 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,27 +1,28 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -use crate::state::AppState; -use crate::utils::storage::setup_storage; -use crate::commands::container::{container_operation, container_stats, fetch_container_info, fetch_containers, get_container, stream_docker_logs}; +use crate::commands::container::{ + container_operation, container_stats, fetch_container_info, fetch_containers, get_container, + rename_container, stream_docker_logs, +}; use crate::commands::extra::{cancel_stream, get_version, ping}; use crate::commands::image::{delete_image, export_image, image_history, image_info, list_images}; use crate::commands::network::{inspect_network, list_networks}; use crate::commands::volume::{inspect_volume, list_volumes}; use crate::commands::terminal::get_available_terminals; +use crate::state::AppState; +use crate::utils::storage::setup_storage; -mod state; -mod utils; mod commands; mod constants; +mod state; +mod utils; fn main() { std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); let state = AppState::default(); - tauri::Builder::default() .manage(state) .plugin(tauri_plugin_store::Builder::default().build()) @@ -50,6 +51,7 @@ fn main() { get_version, ping, get_available_terminals, + rename_container ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/Icons/index.jsx b/src/Icons/index.jsx index 93716c3..e9c8e9e 100644 --- a/src/Icons/index.jsx +++ b/src/Icons/index.jsx @@ -436,3 +436,58 @@ export function IconGithub(props) { ); } + + +export function IconEdit(props) { + return ( + + + + + ) +} + + +export function IconTick(props) { + return ( + + + + ); +} + + +export function IconCancel(props) { + return ( + + + + + ) +} \ No newline at end of file diff --git a/src/components/Containers/ContainerDetails.jsx b/src/components/Containers/ContainerDetails.jsx index d841d66..c82a3c4 100644 --- a/src/components/Containers/ContainerDetails.jsx +++ b/src/components/Containers/ContainerDetails.jsx @@ -10,199 +10,204 @@ import {useContainers} from '../../state/ContainerContext'; import LogoScreen from '../LogoScreen'; import ContainerStats from './ContainerStats'; import JSONSyntaxHighlighter from "../JSONSyntaxHighlighter.jsx"; +import ContainerNameWidget from "./ContainerNameWidget.jsx"; function ContainerDetails() { - const { selectedContainer, setSelectedContainer } = useContainers() + const {selectedContainer, refreshSelectedContainer} = useContainers() - const [activeTab, setActiveTab] = useState('LOGS'); - const [info, setInfo] = useState(""); - const [logs, setLogs] = useState([]); + const [activeTab, setActiveTab] = useState('LOGS'); + const [info, setInfo] = useState(""); + const [logs, setLogs] = useState([]); - const [isContainerRunning, setIsContainerRunning] = useState(false) + const [isContainerRunning, setIsContainerRunning] = useState(false) - const [loadingButton, setLoadingButton] = useState(null) + const [loadingButton, setLoadingButton] = useState(null) - useEffect(() => { - if (selectedContainer) { - setIsContainerRunning(selectedContainer.Status.toLowerCase().includes("up")) + useEffect(() => { + if (selectedContainer) { - setLogs([]); // Clear logs before subscribing + setIsContainerRunning(selectedContainer.Status.toLowerCase().includes("up")) - const unlistenLogs = listen('log_chunk', (event) => { + setLogs([]); // Clear logs before subscribing - const sanitizedLog = sanitizeLog(event.payload); - setLogs((prevLogs) => [...prevLogs, sanitizedLog]); - }); + const unlistenLogs = listen('log_chunk', (event) => { + const sanitizedLog = sanitizeLog(event.payload); + setLogs((prevLogs) => [...prevLogs, sanitizedLog]); + }); - invoke('stream_docker_logs', { containerName: selectedContainer.Names[0].replace("/", "") }); + invoke('stream_docker_logs', {containerName: selectedContainer.Names[0].replace("/", "")}); - return () => { - unlistenLogs.then(f => f()); - }; - } - }, [selectedContainer]); + return () => { + unlistenLogs.then(f => f()); + }; + } + }, [selectedContainer]); - useEffect(() => { - if (activeTab === 'INFO' && selectedContainer) { - getInfo(); - } + useEffect(() => { + if (activeTab === 'INFO' && selectedContainer) { + getInfo(); + } + }, [activeTab, selectedContainer]); - }, [activeTab, selectedContainer]); + useEffect(() => { + if (selectedContainer) { + const intervalId = setInterval(() => { + refreshSelectedContainer(); + }, 60000); // 60000 milliseconds = 1 minute + // Clean up function to clear the interval when the component unmounts + // or when selectedContainer changes + return () => { + clearInterval(intervalId); + invoke('cancel_stream', {streamType: "logs"}); + }; + } + }, [selectedContainer]); - useEffect(() => { - if (selectedContainer) { - const intervalId = setInterval(() => { - refreshSelectContainer(); - }, 60000); // 60000 milliseconds = 1 minute - // Clean up function to clear the interval when the component unmounts - // or when selectedContainer changes - return () => { - clearInterval(intervalId); - invoke('cancel_stream', { streamType: "logs" }); - }; + function getInfo() { + invoke('fetch_container_info', {cId: selectedContainer.Id}).then((info) => { + setInfo(info); + console.log("Fetched container info:", info); + }).catch((error) => { + console.error("Error fetching container info:", error); + }); } - }, [selectedContainer]); - - - - function getInfo() { - invoke('fetch_container_info', { cId: selectedContainer.Id }).then((info) => { - setInfo(info); - console.log("Fetched container info:", info); - }).catch((error) => { - console.error("Error fetching container info:", error); - }); - } - - - - function sanitizeLog(log) { - return log.replace(/[\x00-\x1F\x7F]/g, ""); - } - - const isWeb = () => { - return selectedContainer.Ports.length > 0 && selectedContainer.Ports[0].PublicPort !== null; - }; - - function refreshSelectContainer() { - invoke('get_container', { cId: selectedContainer.Id }).then((res) => { - - if (res) { - setSelectedContainer(res) - } - - }) - } - - function containerOperation(actionType) { - setLoadingButton(actionType) - invoke('container_operation', { containerName: selectedContainer.Names[0].replace("/", ""), opType: actionType }).then((res) => { - if (res) { - toast.success(res); - - refreshSelectContainer() - } - }).catch((e) => { - toast.error(e); - }).finally(() => { - setLoadingButton(null) - }); - } - - const renderContent = () => { - switch (activeTab) { - case 'LOGS': - return ; - case 'INFO': - return ; - case 'STATS': - return ; - default: - return null; + + + function sanitizeLog(log) { + return log.replace(/[\x00-\x1F\x7F]/g, ""); } - }; - - if (selectedContainer == null) { - return - - } - - return ( -
-
-

{selectedContainer.Names[0].slice(1)}

-

Status: {selectedContainer.Status}

-
-
-
- -
-
- -
-
- {isContainerRunning ? - - : - } -
+ const isWeb = () => { + return selectedContainer.Ports.length > 0 && selectedContainer.Ports[0].PublicPort !== null; + }; + + + function containerOperation(actionType) { + setLoadingButton(actionType) + invoke('container_operation', { + containerName: selectedContainer.Names[0].replace("/", ""), + opType: actionType + }).then((res) => { + if (res) { + toast.success(res); + + refreshSelectedContainer() + } + }).catch((e) => { + toast.error(e); + }).finally(() => { + setLoadingButton(null) + }); + } -
- -
+ const renderContent = () => { + switch (activeTab) { + case 'LOGS': + return ; + case 'INFO': + return ; + case 'STATS': + return ; + default: + return null; + } + }; + + if (selectedContainer == null) { + return + + } -
- + return ( +
+
+ + +

Status: {selectedContainer.Status}

+
+
+
+ +
+
+ +
+ +
+ {isContainerRunning ? + + : + } +
+ +
+ +
+ +
+ +
+
+
+ + + +
+
+ {renderContent()} +
-
-
- - - -
-
- {renderContent()} -
-
- ); + ); } export default ContainerDetails; diff --git a/src/components/Containers/ContainerNameWidget.jsx b/src/components/Containers/ContainerNameWidget.jsx new file mode 100644 index 0000000..7f59846 --- /dev/null +++ b/src/components/Containers/ContainerNameWidget.jsx @@ -0,0 +1,94 @@ +import {IconCancel, IconEdit, IconTick} from "../../Icons/index.jsx"; +import React, {useState} from "react"; +import {invoke} from "@tauri-apps/api"; +import {useContainers} from "../../state/ContainerContext.jsx"; +import {toast} from "react-toastify"; + +export default function ContainerNameWidget() { + const {selectedContainer, refreshSelectedContainer} = useContainers(); + + const [isEditingName, setIsEditingName] = useState(false); + const [newContainerName, setNewContainerName] = useState( + selectedContainer.Names[0].replace("/", "") + ); + + const handleNameUpdate = () => { + + // If the new name is the same as the old name, do nothing + if (newContainerName === selectedContainer.Names[0].replace("/", "")) { + setIsEditingName(false); + return; + } + + invoke('rename_container', { + name: selectedContainer.Names[0].replace("/", ""), + newName: newContainerName + }) + .then((res) => { + refreshSelectedContainer(); + setIsEditingName(false); + toast.success(res); + }) + .catch((error) => { + toast.error('Failed to update container name'); + console.error(error); + }) + .finally(() => setIsEditingName(false)); + }; + + const startEditing = () => { + setNewContainerName(selectedContainer.Names[0].replace("/", "")); + setIsEditingName(true); + }; + + const cancelEditing = () => { + setNewContainerName(selectedContainer.Names[0].replace("/", "")); + setIsEditingName(false); + }; + + return ( +
+ {isEditingName ? ( + setNewContainerName(e.target.value)} + className="text-lg font-bold bg-transparent border-b border-gray-300 focus:outline-none focus:border-gray-600" + autoFocus + onBlur={() => setTimeout(() => setIsEditingName(false), 100)} + /> + ) : ( +

+ {selectedContainer.Names[0].replace("/", "")} +

+ )} + {!isEditingName && ( + + )} + {isEditingName && ( +
+ + +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/state/ContainerContext.jsx b/src/state/ContainerContext.jsx index 3cbd10c..dfd1f23 100644 --- a/src/state/ContainerContext.jsx +++ b/src/state/ContainerContext.jsx @@ -13,9 +13,20 @@ export function ContainerProvider({children}) { }); }, []); + function refreshSelectedContainer() { + invoke('get_container', {cId: selectedContainer.Id}).then((res) => { + + if (res) { + setSelectedContainer(res) + } + + }) + } + return ( - + {children} );