Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/api/instances/[name]/plugins/PluginsRoute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ it("sends SSM commands and returns success message when plugin update succeeds",
mockedFetchInstance.mockResolvedValueOnce(dummyInstance);
mockedRunSSMCommands.mockResolvedValueOnce("test-stdout");

const payload = { name: "rabbitmq_management", enabled: true };
const payload = { updates: [{ name: "rabbitmq_management", enabled: true }] };
const { req: originalReq } = createMocks({
method: "POST",
url: "/api/instances/test-instance/plugins?region=us-east-1",
Expand Down
51 changes: 30 additions & 21 deletions src/app/api/instances/[name]/plugins/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { NextRequest, NextResponse } from "next/server";

// import { getPlugins, togglePlugins } from "./service";
import getPlugins from "./utils/getPlugins";
import togglePlugins from "./utils/togglePlugins";
import eventEmitter from "@/utils/eventEmitter";
import { deleteEvent } from "@/utils/eventBackups";
import { PluginUpdate } from "./types";

export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ name: string }> },
{ params }: { params: Promise<{ name: string }> }
) {
const searchParams = request.nextUrl.searchParams;
const region = searchParams.get("region");
Expand All @@ -20,7 +20,7 @@ export async function GET(
if (!region || !username || !password) {
return NextResponse.json(
{ message: "Missing parameters" },
{ status: 400 },
{ status: 400 }
);
}

Expand All @@ -34,17 +34,20 @@ export async function GET(

return NextResponse.json(plugins);
} catch (error) {
console.error("Error fetching plugins:", error);
return NextResponse.json(
{ message: "Error fetching plugins", error: String(error) },
{ status: 500 },
{
message: `Error fetching plugins\n${
error instanceof Error ? error.message : String(error)
}`,
},
{ status: 500 }
);
}
}

export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ name: string }> },
{ params }: { params: Promise<{ name: string }> }
) {
const searchParams = request.nextUrl.searchParams;
const region = searchParams.get("region");
Expand All @@ -53,28 +56,30 @@ export async function POST(
if (!region) {
return NextResponse.json(
{ message: "Missing region parameter" },
{ status: 400 },
{ status: 400 }
);
}

const { name, enabled } = (await request.json()) as {
name: string;
enabled: boolean;
};

try {
const { updates } = (await request.json()) as {
updates?: PluginUpdate[];
};

if (!Array.isArray(updates) || updates.length === 0) {
throw new Error("No updates provided");
}

await togglePlugins({
region,
pluginName: name,
enabled,
updates,
instanceName,
});

eventEmitter.emit("notification", {
type: "plugin",
status: "success",
instanceName,
message: `${enabled ? "Enabled" : "Disabled"} ${name} on ${instanceName}`,
instanceName: instanceName,
message: `Updated ${updates.length} plugin(s) on instance ${instanceName}`,
});

deleteEvent(instanceName, "plugin");
Expand All @@ -86,14 +91,18 @@ export async function POST(
type: "plugin",
status: "error",
instanceName: instanceName,
message: "Error updating plugin",
message: `Error updating plugins on instance ${instanceName}`,
});

deleteEvent(instanceName, "plugin");
console.error("Error updating plugins:", error);

return NextResponse.json(
{ message: "Error updating plugins", error: String(error) },
{ status: 500 },
{
message: `Error updating plugins\n${
error instanceof Error ? error.message : String(error)
}`,
},
{ status: 500 }
);
}
}
8 changes: 6 additions & 2 deletions src/app/api/instances/[name]/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ export interface GetPluginsParams {

export interface TogglePluginsParams {
region: string;
pluginName: string;
enabled: boolean;
updates: PluginUpdate[];
instanceName: string;
}

export interface PluginUpdate {
name: string;
enabled: boolean;
}
8 changes: 7 additions & 1 deletion src/app/api/instances/[name]/plugins/utils/getPlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,11 @@ export default async function getPlugins({
throw new Error("No response from RabbitMQ API");
}

return response.data[0].enabled_plugins || [];
if (!response.data[0].enabled_plugins) {
throw new Error(
"RabbitMQ node data is not ready yet, please refresh later"
);
}

return response.data[0].enabled_plugins;
}
13 changes: 5 additions & 8 deletions src/app/api/instances/[name]/plugins/utils/togglePlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { runSSMCommands } from "@/utils/AWS/SSM/runSSMCommands";

export default async function togglePlugins({
region,
pluginName,
enabled,
updates,
instanceName,
}: TogglePluginsParams): Promise<void> {
const ec2Client = new EC2Client({ region });
Expand All @@ -16,12 +15,10 @@ export default async function togglePlugins({
throw new Error(`No instance found with name: ${instanceName}`);
}
const instanceId = instance.InstanceId;
const commands: string[] = [];
if (enabled) {
commands.push(`rabbitmq-plugins enable ${pluginName}`);
} else {
commands.push(`rabbitmq-plugins disable ${pluginName}`);
}
const commands = updates.map(
({ name, enabled }) =>
`rabbitmq-plugins ${enabled ? "enable" : "disable"} ${name}`
);

commands.push("systemctl restart rabbitmq-server");

Expand Down
43 changes: 27 additions & 16 deletions src/app/api/instances/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { NextRequest, NextResponse } from "next/server";
import { pollRabbitMQServerStatus } from "@/utils/RabbitMQ/serverStatus";
import createInstance from "@/utils/AWS/EC2/createBrokerInstance";
import listInstances from "./utils/listInstances";
import { formattedInstances } from "./utils/utils";
import {
formattedInstances,
isInstanceNameUnique,
validBody,
} from "./utils/utils";

import eventEmitter from "@/utils/eventEmitter";
import { deleteEvent } from "@/utils/eventBackups";
Expand Down Expand Up @@ -33,20 +37,23 @@ export const POST = async (request: NextRequest) => {
storageSize,
} = await request.json();

if (
!region ||
!instanceName ||
!instanceType ||
!username ||
!password ||
!storageSize
) {
return NextResponse.json(
{ message: "Invalid request body" },
{ status: 400 }
);
}
try {
if (
!validBody(
instanceName,
region,
instanceType,
username,
password,
storageSize
)
) {
throw new Error("Invalid request body");
}

if (!(await isInstanceNameUnique(instanceName))) {
throw new Error(`Instance ${instanceName} already exists`);
}
const createInstanceResult = await createInstance(
region,
instanceName,
Expand Down Expand Up @@ -81,9 +88,13 @@ export const POST = async (request: NextRequest) => {
});

deleteEvent(instanceName, "newInstance");
console.error("Error creating instance:", error);

return NextResponse.json(
{ message: "Error creating instance" },
{
message: `Error creating instance\n${
error instanceof Error ? error.message : String(error)
}`,
},
{ status: 500 }
);
}
Expand Down
34 changes: 34 additions & 0 deletions src/app/api/instances/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { InstanceWithRegion, FormattedInstance } from "../types";
import listInstances from "./listInstances";

export function formattedInstances(
instances: InstanceWithRegion[]
Expand All @@ -19,3 +20,36 @@ export function formattedInstances(
})
.filter(Boolean);
}

export async function isInstanceNameUnique(
instanceName: string
): Promise<boolean> {
const instances = await listInstances();
return !instances.some((instance) =>
instance.Tags?.some(
(tag) => tag.Key === "Name" && tag.Value === instanceName
)
);
}

export function validBody(
instanceName: string,
region: string,
instanceType: string,
username: string,
password: string,
storageSize: number
): boolean {
return (
/^[a-z0-9-_]{3,64}$/i.test(instanceName) &&
region.length > 0 &&
instanceType.length > 0 &&
username.length >= 6 &&
password.length >= 8 &&
/[a-zA-Z]/.test(password) &&
/[0-9]/.test(password) &&
/[!@#$%^&*]/.test(password) &&
storageSize >= 8 &&
storageSize <= 16000
);
}
46 changes: 27 additions & 19 deletions src/app/components/NotificationsDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { useNotificationsContext } from "../NotificationContext";
import { NotificationStatus } from "@/types/notification";
import { Bell, X } from "lucide-react";


export default function NotificationsDropdown() {
const { notifications, updateNotification, deleteNotification } = useNotificationsContext();
const { notifications, updateNotification, deleteNotification } =
useNotificationsContext();
const [showDropdown, setShowDropdown] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);

Expand Down Expand Up @@ -61,7 +61,10 @@ export default function NotificationsDropdown() {

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setShowDropdown(false);
}
};
Expand Down Expand Up @@ -91,7 +94,9 @@ export default function NotificationsDropdown() {
{showDropdown && (
<div className="absolute top-full right-0 mt-2 bg-card border border-mainbg1 z-50 w-64 p-2 rounded-sm">
{notificationCount === 0 ? (
<div className="font-text1 text-pagetext1 text-sm p-1">No notifications</div>
<div className="font-text1 text-pagetext1 text-sm p-1">
No notifications
</div>
) : (
<ul className="text-sm space-y-2">
{notifications.map((notification, index) => (
Expand All @@ -106,21 +111,24 @@ export default function NotificationsDropdown() {
notification.status
)}`}
/>
<span className="flex-1 break-words">{notification.message}</span>
<button
onClick={() =>
deleteNotification(
notification.type,
notification.instanceName,
notification.message,
index
)
}
className="ml-2 text-pagetext1 hover:text-card text-xs"
disabled={notification.status === "pending"}
>
<X size={16} />
</button>
<span className="flex-1 break-words">
{notification.message}
</span>
{!(notification.status === "pending") && (
<button
onClick={() =>
deleteNotification(
notification.type,
notification.instanceName,
notification.message,
index
)
}
className="ml-2 text-pagetext1 hover:text-card text-xs"
>
<X size={16} />
</button>
)}
</li>
))}
</ul>
Expand Down
Loading