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
3 changes: 2 additions & 1 deletion src/app/monitoring/app-monitoring.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Button } from '@/components/ui/button';
import { AppMonitoringUsageModel } from '@/shared/model/app-monitoring-usage.model';
import PodStatusIndicator from '@/components/custom/pod-status-indicator';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { PathBuilderUtils } from '@/shared/utils/path-builder.utils';

export default function AppRessourceMonitoring({
appsRessourceUsage
Expand Down Expand Up @@ -74,7 +75,7 @@ export default function AppRessourceMonitoring({
</TableCell>
<TableCell>{KubeSizeConverter.convertBytesToReadableSize(item.ramUsageBytes)}</TableCell>
<TableCell>
<Link href={`/project/app/${item.appId}`} >
<Link href={PathBuilderUtils.projectAppDrawer(item.projectId, item.appId, 'stats')} >
<Button variant="ghost" size="sm">
<ExternalLink />
</Button>
Expand Down
3 changes: 2 additions & 1 deletion src/app/monitoring/app-volumes-monitoring.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ExternalLink } from 'lucide-react';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Progress } from "@/components/ui/progress"
import { PathBuilderUtils } from '@/shared/utils/path-builder.utils';

type AppVolumeMonitoringUsageExtendedModel = AppVolumeMonitoringUsageModel & {
usedPercentage: number;
Expand Down Expand Up @@ -92,7 +93,7 @@ export default function AppVolumeMonitoring({
<div className='text-xs text-slate-500'>{KubeSizeConverter.convertBytesToReadableSize(item.usedBytes)} / {KubeSizeConverter.convertBytesToReadableSize(item.capacityBytes)}</div>
</TableCell>
<TableCell>
<Link href={`/project/app/${item.appId}?tabName=storage`} >
<Link href={PathBuilderUtils.projectAppDrawer(item.projectId, item.appId, 'settings')} >
<Button variant="ghost" size="sm">
<ExternalLink />
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
'use client';

import { useEffect, useState } from 'react';
import { Archive } from 'lucide-react';
import { Archive, Download, Loader2 } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
import LoadingSpinner from '@/components/ui/loading-spinner';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { formatDateTime } from '@/frontend/utils/format.utils';
import { Actions } from '@/frontend/utils/nextjs-actions.utils';
import { Toast } from '@/frontend/utils/toast.utils';
import { KubeSizeConverter } from '@/shared/utils/kubernetes-size-converter.utils';
import type { BackupEntry } from '@/shared/model/backup-info.model';
import type { VolumeBackupExtendedModel } from '@/shared/model/volume-backup-extended.model';
import { getBackupsForVolumeSchedule } from '@/app/project/app/[appId]/volumes/actions';
import {
downloadBackupForVolumeSchedule,
getBackupsForVolumeSchedule,
} from '@/app/project/app/[appId]/volumes/actions';

export function DrawerBackupList({
volumeBackup,
Expand All @@ -20,6 +25,7 @@ export function DrawerBackupList({
}) {
const [backups, setBackups] = useState<BackupEntry[]>();
const [hasLoadError, setHasLoadError] = useState(false);
const [downloadingBackupKey, setDownloadingBackupKey] = useState<string>();

useEffect(() => {
setBackups(undefined);
Expand All @@ -29,6 +35,21 @@ export function DrawerBackupList({
.catch(() => setHasLoadError(true));
}, [volumeBackup.id]);

const downloadBackup = async (backupKey: string) => {
try {
setDownloadingBackupKey(backupKey);
const result = await Toast.fromAction(() =>
downloadBackupForVolumeSchedule(volumeBackup.id, backupKey),
);
if (result.status === 'success' && result.data) {
const params = new URLSearchParams({ fileName: result.data });
window.open(`/api/volume-data-download?${params.toString()}`);
}
} finally {
setDownloadingBackupKey(undefined);
}
};

if (hasLoadError) {
return (
<Alert variant="destructive">
Expand Down Expand Up @@ -70,6 +91,7 @@ export function DrawerBackupList({
<TableRow>
<TableHead>Created</TableHead>
<TableHead>Size</TableHead>
<TableHead><span className="sr-only">Actions</span></TableHead>
</TableRow>
</TableHeader>
<TableBody>
Expand All @@ -81,6 +103,22 @@ export function DrawerBackupList({
? KubeSizeConverter.convertBytesToReadableSize(backup.sizeBytes)
: 'Unknown'}
</TableCell>
<TableCell className="text-right">
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => void downloadBackup(backup.key)}
disabled={downloadingBackupKey !== undefined}
>
{downloadingBackupKey === backup.key ? (
<Loader2 className="animate-spin" />
) : (
<Download />
)}
<span className="sr-only">Download backup</span>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { AppExtendedModel } from '@/shared/model/app-extended.model';
import { RolePermissionEnum } from '@/shared/model/role-extended.model.ts';
import type { VolumeBackupExtendedModel } from '@/shared/model/volume-backup-extended.model';
import { useNestedDrawer } from './nested-drawer';
import { DrawerBackupList } from './drawer-backup-list';
import { DrawerBackupList } from './app-details-backup-list';

export function DrawerBackupsTab({
app,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ import WebhookDeploymentInfo from "@/app/project/app/[appId]/overview/webhook-de
import { Button } from "@/components/ui/button";
import { Webhook } from "lucide-react";




export default function DrawerDeploymentsTab({
app,
role,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ import {
DrawerTitle,
} from '@/components/ui/drawer';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty';
import PodStatusIndicator from '@/components/custom/pod-status-indicator';
import { deploy, startApp, stopApp } from '@/app/project/app/[appId]/actions';
import { usePodsStatus } from '@/frontend/states/zustand.states';
Expand All @@ -51,28 +58,27 @@ import type { AppExtendedModel } from '@/shared/model/app-extended.model';
import Logs from '@/app/project/app/[appId]/overview/logs';
import MonitoringTab from '@/app/project/app/[appId]/overview/monitoring-app';
import { RolePermissionEnum } from '@/shared/model/role-extended.model.ts';
import type { NetworkGraphNode } from './project-network-graph-projection';
import GeneralAppSource from '@/app/project/app/[appId]/general/app-source';
import DbCredentials from '@/app/project/app/[appId]/credentials/db-crendentials';
import DbToolsCard from '@/app/project/app/[appId]/credentials/db-tools';
import { DrawerSettings } from './drawer/drawer-settings';
import { NestedDrawerProvider } from './drawer/nested-drawer';
import { DrawerBackupsTab } from './drawer/drawer-backups-tab';
import { EditAppDialog } from '../edit-app-dialog';
import { DrawerSettings } from './app-details-settings';
import { NestedDrawerProvider } from './nested-drawer';
import { DrawerBackupsTab } from './app-details-backups-tab';
import { EditAppDialog } from '@/app/project/[projectId]/app-components/edit-app-dialog';
import type { S3Target } from '@prisma/client';
import type { VolumeBackupExtendedModel } from '@/shared/model/volume-backup-extended.model';
import {
DrawerSessionUtils,
type DrawerTab,
} from './project-network-graph-drawer-session';
import DrawerDeploymentsTab from './drawer/drawer-deployments-tab';
AppDrawerNavigationUtils,
type AppDrawerTab,
} from '@/shared/utils/app-drawer-navigation.utils';
import DrawerDeploymentsTab from './app-details-deployments-tab';

export type PanelConnection = {
export type AppDetailsDrawerNode = {
id: string;
kind: string;
external?: boolean;
name: string;
direction: 'Ingress' | 'Egress';
label?: string;
copyValue?: string;
caption?: string;
};

function DrawerTabScrollArea({ children }: { children: ReactNode }) {
Expand Down Expand Up @@ -234,7 +240,7 @@ function AppStatusActions({
);
}

export function NodeDetailsDrawer({
export function AppDetailsDrawer({
contentRef,
node,
app,
Expand All @@ -248,9 +254,11 @@ export function NodeDetailsDrawer({
onOpenChangeComplete,
requestedTab,
onTabChange,
openEnvironment,
onEnvironmentOpened,
}: {
contentRef?: Ref<HTMLDivElement>;
node: NetworkGraphNode;
node: AppDetailsDrawerNode;
app?: AppExtendedModel;
role?: RolePermissionEnum;
s3Targets: S3Target[];
Expand All @@ -261,23 +269,26 @@ export function NodeDetailsDrawer({
onOpenChange: (open: boolean) => void;
onOpenChangeComplete: (open: boolean) => void;
requestedTab?: string | null;
onTabChange: (tab: DrawerTab) => void;
onTabChange: (tab: AppDrawerTab) => void;
openEnvironment?: boolean;
onEnvironmentOpened?: () => void;
}) {
const isApp = node.kind === 'APP';
const isExternalApp = isApp && node.external;
const needsSourceConfiguration =
app &&
role === RolePermissionEnum.READWRITE &&
!AppSourceUtils.isConfiguredSource(app);
const hasVolumes = (app?.appVolumes.length ?? 0) > 0;

const activeTab = DrawerSessionUtils.resolveTab(
const activeTab = AppDrawerNavigationUtils.resolveTab(
app?.appType,
requestedTab,
hasVolumes,
);

const handleTabChange = (tab: string) => {
const nextTab = DrawerSessionUtils.resolveTab(app?.appType, tab, hasVolumes);
const nextTab = AppDrawerNavigationUtils.resolveTab(app?.appType, tab, hasVolumes);
onTabChange(nextTab);
};

Expand Down Expand Up @@ -394,7 +405,19 @@ export function NodeDetailsDrawer({
<div className="h-2"></div>
)}
</DrawerHeader>
{needsSourceConfiguration ? (
{isExternalApp ? (
<Empty className="border-0 rounded-none">
<EmptyHeader>
<EmptyMedia variant="icon">
<Settings />
</EmptyMedia>
<EmptyTitle>Settings unavailable</EmptyTitle>
<EmptyDescription>
This app belongs to another project. Its settings can only be edited from that project.
</EmptyDescription>
</EmptyHeader>
</Empty>
) : needsSourceConfiguration ? (
<DrawerTabScrollArea>
<GeneralAppSource
hideCard
Expand Down Expand Up @@ -451,6 +474,8 @@ export function NodeDetailsDrawer({
role={role}
storageClasses={storageClasses}
gitSshPublicKey={gitSshPublicKey}
openEnvironment={openEnvironment}
onEnvironmentOpened={onEnvironmentOpened}
/>
</div>
</DrawerTabScrollArea>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
SlidersHorizontal,
Zap,
} from 'lucide-react';
import { useCallback, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import BasicAuth from '@/app/project/app/[appId]/advanced/basic-auth';
Expand All @@ -25,22 +26,39 @@ import FileMountsCard from '@/components/custom/file-mounts-card';
import type { AppExtendedModel } from '@/shared/model/app-extended.model';
import { RolePermissionEnum } from '@/shared/model/role-extended.model.ts';
import { SettingsSection } from './settings-section';
import { DrawerEnvironment } from './drawer-environment';
import { DrawerEnvironment } from './app-details-environment';
import { useNestedDrawer } from './nested-drawer';

export function DrawerSettings({
app,
role,
storageClasses,
gitSshPublicKey,
openEnvironment = false,
onEnvironmentOpened,
}: {
app: AppExtendedModel;
role: RolePermissionEnum;
storageClasses: string[];
gitSshPublicKey?: string;
openEnvironment?: boolean;
onEnvironmentOpened?: () => void;
}) {
const readonly = role !== RolePermissionEnum.READWRITE;
const { openNestedDrawer } = useNestedDrawer();
const openEnvironmentEditor = useCallback(() => {
openNestedDrawer({
title: 'Environment variables',
description: <>Values are encrypted at rest and never shown. Names starting with <code className="rounded bg-muted px-1 text-xs">QS_</code> are reserved by QuickStack.</>,
content: <EnvEdit app={app} readonly={readonly} hideCard={true} />,
});
}, [app, openNestedDrawer, readonly]);

useEffect(() => {
if (!openEnvironment) return;
openEnvironmentEditor();
onEnvironmentOpened?.();
}, [onEnvironmentOpened, openEnvironment, openEnvironmentEditor]);

return (
<div className="relative grid gap-16 grid-cols-1 lg:grid-cols-[1fr_auto]">
Expand Down Expand Up @@ -88,18 +106,7 @@ export function DrawerSettings({
>
<DrawerEnvironment
app={app}
onEdit={() =>
openNestedDrawer({
title: 'Environment variables',
content: (
<EnvEdit
app={app}
readonly={readonly}
hideCard={false}
/>
),
})
}
onEdit={openEnvironmentEditor}
/>
</SettingsSection>
<SettingsSection
Expand Down
9 changes: 6 additions & 3 deletions src/app/project/[projectId]/app-components/apps-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { EditAppDialog } from "./edit-app-dialog";
import { UserSession } from "@/shared/model/sim-session.model";
import { UserGroupUtils } from "@/shared/utils/role.utils";
import PodStatusIndicator from "@/components/custom/pod-status-indicator";
import { PathBuilderUtils } from "@/shared/utils/path-builder.utils";


export default function AppTable({
Expand All @@ -28,11 +29,13 @@ export default function AppTable({
}) {

const { openConfirmDialog: openDialog } = useConfirmDialog();
const drawerHref = (appId: string) =>
PathBuilderUtils.projectAppDrawer(projectId, appId, 'deployments', 'table');

return <>
<SimpleDataTable columns={[
['id', 'ID', false],
['name', 'Name', true, (item) => <Link href={`/project/apps/${item.id}`}
['name', 'Name', true, (item) => <Link href={drawerHref(item.id)}
className="font-medium cursor-pointer hover:underline">
{item.name}
</Link>],
Expand All @@ -47,7 +50,7 @@ export default function AppTable({
['status', 'Status', true, (item) => <PodStatusIndicator appId={item.id} />],
]}
data={apps}
onItemClickLink={(item) => `/project/app/${item.id}`}
onItemClickLink={(item) => drawerHref(item.id)}
actionCol={(item) =>
<>
<div className="flex">
Expand All @@ -61,7 +64,7 @@ export default function AppTable({
<DropdownMenuGroup>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
</DropdownMenuGroup>
<Link href={`/project/app/${item.id}`}>
<Link href={drawerHref(item.id)}>
<DropdownMenuItem>
<Eye /> <span>Show App Details</span>
</DropdownMenuItem>
Expand Down
Loading
Loading