Commit bfc0ada3 authored by Dimitrios Gogos's avatar Dimitrios Gogos
Browse files

feat: add modals, add deploy and delete functionality for apps

parent e818f78e
Loading
Loading
Loading
Loading
+19 −5
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ import { IApplicationProfile } from "@/app/utils/interfaces";
import { backArrowIcon } from "@/app/utils/icons";
import Loader from "@/app/components/Loader/Loader";
import Error from "@/app/components/Error/Error";
import { DeployModal } from "@/app/components/DeployModal/DeployModal";
import buttons from "@/app/styles/buttons.module.scss";
import styles from "./profilePage.module.scss";

@@ -16,6 +17,7 @@ const ProfilePage = () => {
  const [app, setApp] = useState<IApplicationProfile | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const [deployOpen, setDeployOpen] = useState(false);

  useEffect(() => {
    fetch(`/api/apps/${appId}`)
@@ -41,15 +43,20 @@ const ProfilePage = () => {
        <Button className={buttons.iconSubtle} onClick={() => router.back()}>
          {backArrowIcon}
        </Button>
        <Button className={buttons.primary} onClick={() => setDeployOpen(true)}>
          Deploy
        </Button>
      </div>

      <div className={styles.header}>
        <div className={styles.headerTop}>
          <div className={styles.badges}>
            <span className={styles.badge}>{app.packageType}</span>
            {app.appProvider && (
              <span className={styles.badgeSecondary}>{app.appProvider}</span>
            )}
          </div>
        </div>
        <h1 className={styles.title}>{app.name}</h1>
        <p className={styles.appId}>appID: {app.appId}</p>
      </div>
@@ -125,6 +132,13 @@ const ProfilePage = () => {
          </pre>
        </>
      )}
      <DeployModal
        open={deployOpen}
        appId={app.appId}
        appName={app.name}
        onClose={() => setDeployOpen(false)}
        onDeployed={() => setDeployOpen(false)}
      />
    </div>
  );
};
+3 −1
Original line number Diff line number Diff line
@@ -9,7 +9,8 @@

.btnBox {
  display: flex;
  justify-content: flex-end;
  justify-content: space-between;
  align-items: center;
}

.header {
@@ -23,6 +24,7 @@
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  align-items: center;
}

.badge {
+87 −54
Original line number Diff line number Diff line
@@ -5,6 +5,8 @@ import React, { useState } from "react";
import styles from "./myApplications.module.scss";
import { truncate } from "../../utils/helpers";
import { IApplicationProfile } from "../../utils/interfaces";
import { DeployModal } from "../../components/DeployModal/DeployModal";
import { ConfirmModal } from "../../components/ConfirmModal/ConfirmModal";

type Props = {
  app: IApplicationProfile;
@@ -12,22 +14,24 @@ type Props = {
};

export const ProfileCard = ({ app, onDelete }: Props) => {
  const [deployOpen, setDeployOpen] = useState(false);
  const [deleteOpen, setDeleteOpen] = useState(false);
  const [deleting, setDeleting] = useState(false);
  const [deleteError, setDeleteError] = useState<string | null>(null);

  const handleDelete = async (e: React.MouseEvent) => {
    e.preventDefault();
    e.stopPropagation();
    if (!confirm(`Delete "${app.name}"?`)) return;
  const handleDelete = async () => {
    setDeleting(true);
    setDeleteError(null);
    try {
      const res = await fetch(`/api/apps/${app.appId}`, { method: "DELETE" });
      if (res.ok) {
        onDelete(app.appId);
        setDeleteOpen(false);
      } else {
        alert("Failed to delete application.");
        setDeleteError("Failed to delete application.");
      }
    } catch {
      alert("Could not reach the server.");
      setDeleteError("Could not reach the server.");
    } finally {
      setDeleting(false);
    }
@@ -38,6 +42,7 @@ export const ProfileCard = ({ app, onDelete }: Props) => {
  );

  return (
    <>
      <Link href={`/app-profiles/${app.appId}`} className={styles.card}>

        <div className={styles.cardHeader}>
@@ -47,15 +52,23 @@ export const ProfileCard = ({ app, onDelete }: Props) => {
              <span className={styles.badgeSecondary}>{app.appProvider}</span>
            )}
          </div>
          <div className={styles.cardActions}>
            <button
              className={styles.deployBtn}
              onClick={(e) => { e.preventDefault(); e.stopPropagation(); setDeployOpen(true); }}
              aria-label="Deploy application"
            >
              Deploy
            </button>
            <button
              className={styles.deleteBtn}
          onClick={handleDelete}
          disabled={deleting}
              onClick={(e) => { e.preventDefault(); e.stopPropagation(); setDeleteOpen(true); }}
              aria-label="Delete application"
            >
          {deleting ? "Deleting…" : "Delete profile"}
              Delete
            </button>
          </div>
        </div>

        <h2 className={styles.title}>{app.name}</h2>

@@ -94,5 +107,25 @@ export const ProfileCard = ({ app, onDelete }: Props) => {
          )}
        </dl>
      </Link>

      <DeployModal
        open={deployOpen}
        appId={app.appId}
        appName={app.name}
        onClose={() => setDeployOpen(false)}
        onDeployed={() => setDeployOpen(false)}
      />

      <ConfirmModal
        open={deleteOpen}
        title="Delete application profile"
        message={`Are you sure you want to delete "${app.name}"?`}
        confirmLabel="Delete"
        loading={deleting}
        error={deleteError}
        onConfirm={handleDelete}
        onClose={() => { setDeleteOpen(false); setDeleteError(null); }}
      />
    </>
  );
};
+23 −0
Original line number Diff line number Diff line
@@ -105,6 +105,29 @@
        align-items: center;
    }

    .cardActions {
        display: flex;
        gap: 0.4rem;
    }

    .deployBtn {
        background: var(--main-gradient);
        border: none;
        border-radius: 4px;
        color: #fff;
        font-size: 0.72rem;
        font-weight: 700;
        padding: 3px 10px;
        cursor: pointer;
        white-space: nowrap;
        line-height: 1.4;
        transition: opacity 0.15s ease;

        &:hover {
            opacity: 0.85;
        }
    }

    .deleteBtn {
        background: none;
        border: 1px solid #e0e0e0;
+39 −0
Original line number Diff line number Diff line
import { NextResponse } from "next/server";
import { oeg } from "@/app/utils/constants";

export async function POST(req: Request) {
  try {
    const body = await req.json();
    const res = await fetch(`${oeg.baseUrl}/appinstances`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
      cache: "no-store",
    });

    if (!res.ok) {
      const err = await res.json().catch(() => ({}));
      return NextResponse.json(
        { error: err.message ?? `OEG returned ${res.status}` },
        { status: res.status }
      );
    }

    const data = await res.json();

    // OEG returns 202 even on conflict — surface the warning as an error
    if (data.warning || data.details?.error) {
      return NextResponse.json(
        { error: data.details?.error ?? data.warning },
        { status: 409 }
      );
    }

    return NextResponse.json(data, { status: 202 });
  } catch {
    return NextResponse.json(
      { error: "Could not reach OEG" },
      { status: 502 }
    );
  }
}
Loading