diff --git a/src/app/apps/installed/page.tsx b/src/app/apps/installed/page.tsx
index 2d7f290..709b1e6 100644
--- a/src/app/apps/installed/page.tsx
+++ b/src/app/apps/installed/page.tsx
@@ -6,7 +6,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
AlertTriangle,
Boxes,
+ ChevronDown,
CheckCircle,
+ Container,
Cpu,
HardDrive,
Loader2,
@@ -16,11 +18,9 @@ import {
ScrollText,
Server as ServerIcon,
Square,
- Terminal,
Trash2,
Wrench,
X,
- XCircle,
} from "lucide-react";
import DashboardLayout from "@/components/DashboardLayout";
import ConfirmActionDialog from "@/components/ConfirmActionDialog";
@@ -269,6 +269,8 @@ function InstallOperationsModal({
const [error, setError] = useState("");
const [acting, setActing] = useState>({});
const contentViewportRef = useRef(null);
+ const actionsMenuRef = useRef(null);
+ const [actionsMenuOpen, setActionsMenuOpen] = useState(false);
useEffect(() => {
setActiveTab(initialTab);
@@ -295,12 +297,29 @@ function InstallOperationsModal({
void loadRuntime();
}, [loadRuntime]);
+ useEffect(() => {
+ if (!actionsMenuOpen) return;
+
+ const handlePointerDown = (event: PointerEvent) => {
+ if (
+ actionsMenuRef.current &&
+ !actionsMenuRef.current.contains(event.target as Node)
+ ) {
+ setActionsMenuOpen(false);
+ }
+ };
+
+ document.addEventListener("pointerdown", handlePointerDown);
+ return () => document.removeEventListener("pointerdown", handlePointerDown);
+ }, [actionsMenuOpen]);
+
const handleLifecycle = async (action: "start" | "stop" | "restart") => {
if (!install.containerName) {
onToast({
tone: "error",
title: "Installed Apps",
- message: "Container runtime belum ditemukan untuk app ini",
+ message:
+ "Container name is not available for this app install. Lifecycle actions cannot be performed.",
showProgress: true,
});
return;
@@ -314,7 +333,7 @@ function InstallOperationsModal({
onToast({
tone: "success",
title: "Installed Apps",
- message: `${install.appName} ${action} berhasil dijalankan`,
+ message: `${install.appName} ${action} successfully executed`,
showProgress: true,
});
} catch (err: unknown) {
@@ -338,7 +357,7 @@ function InstallOperationsModal({
onToast({
tone: "success",
title: "Installed Apps",
- message: `${install.appName} berhasil direbuild`,
+ message: `${install.appName} successfully rebuilt`,
showProgress: true,
});
} catch (err: unknown) {
@@ -354,31 +373,47 @@ function InstallOperationsModal({
};
const requestRebuildConfirm = () => {
+ setActionsMenuOpen(false);
onRequestConfirm({
title: "Rebuild Installed App",
description: `Rebuild app "${install.appName}" sekarang?`,
confirmLabel: "Rebuild App",
tone: "warning",
- note: "Container app ini akan dihentikan, dihapus, lalu dijalankan ulang. Doktainer akan mencoba menarik image terbaru terlebih dahulu jika tersedia di registry.",
+ note: "Rebuilding will stop the app if it's currently running, and apply any changes from the original app template or the last successful build. This is useful if the app is not working correctly or if you have made changes to the app's source that you want to apply.",
onConfirm: () => {
void handleRebuild();
},
});
};
- const handleOpenContainers = () => {
- storeServerSelection("containers", install.serverId);
- router.push("/containers");
+ const handleOpenEnvironment = () => {
+ setActionsMenuOpen(false);
+
+ if (!install.environment) {
+ onToast({
+ tone: "error",
+ title: "Installed Apps",
+ message:
+ "This app install does not have an associated environment. It might have been installed before this feature was implemented, or there was an issue linking the environment during installation.",
+ showProgress: true,
+ });
+ return;
+ }
+
+ router.push(
+ `/projects/${install.environment.projectId}/environments/${install.environment.id}/containers/${install.environment.containerId}`,
+ );
onClose();
};
- const handleOpenTerminal = () => {
- const encodedName = encodeURIComponent(install.server?.name ?? "Server");
- const encodedIp = encodeURIComponent(install.server?.ip ?? "");
- router.push(
- `/terminal?serverId=${install.serverId}&name=${encodedName}&ip=${encodedIp}`,
- );
- onClose();
+ const handleMenuLifecycle = (action: "start" | "stop" | "restart") => {
+ setActionsMenuOpen(false);
+ void handleLifecycle(action);
+ };
+
+ const handleMenuRemove = () => {
+ setActionsMenuOpen(false);
+ void onRemove(install);
};
const currentStatus = detail?.container.status ?? install.status;
@@ -492,20 +527,6 @@ function InstallOperationsModal({
-
-