Skip to content
Open
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
26 changes: 25 additions & 1 deletion airborne-core-cli/action.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from "fs";
import { promises as fsPromises } from "fs";
import path from "path";
import { CreateApplicationCommand, CreateDimensionCommand, CreateFileCommand, CreateOrganisationCommand, CreatePackageCommand, CreateReleaseCommand, DeleteDimensionCommand, GetReleaseCommand, GetUserCommand, ListDimensionsCommand, ListFileGroupsCommand, ListFilesCommand, ListOrganisationsCommand, ListPackagesCommand, ListReleasesCommand, PostLoginCommand, RequestOrganisationCommand, ServeReleaseCommand, ServeReleaseV2Command, UpdateDimensionCommand, UpdateFileCommand, UploadFileCommand, AirborneClient } from "airborne-server-sdk"
import { CreateApplicationCommand, CreateDimensionCommand, CreateFileCommand, CreateOrganisationCommand, CreatePackageCommand, CreateReleaseCommand, DeleteDimensionCommand, DeleteFileCommand, GetReleaseCommand, GetUserCommand, ListDimensionsCommand, ListFileGroupsCommand, ListFilesCommand, ListOrganisationsCommand, ListPackagesCommand, ListReleasesCommand, PostLoginCommand, RequestOrganisationCommand, ServeReleaseCommand, ServeReleaseV2Command, UpdateDimensionCommand, UpdateFileCommand, UploadFileCommand, AirborneClient } from "airborne-server-sdk"
import { fileURLToPath } from 'url';
import { dirname } from 'path';

Expand Down Expand Up @@ -302,6 +302,30 @@ export async function DeleteDimensionAction(paramsFile, options){
return await client.send(command);
}

export async function DeleteFileAction(paramsFile, options){
let finalOptions = {};
const requiredParams = ["file_id","organisation","application","token"];

if (paramsFile && paramsFile.startsWith('@')) {
const jsonFilePath = paramsFile.slice(1);
finalOptions = mergeOptionsWithJsonFile(options, jsonFilePath, requiredParams);
} else if (paramsFile) {
throw new Error("Params file must start with @ (e.g., @params.json)");
} else {
finalOptions = options;
}

// Validate that all required options are present
validateRequiredOptions(finalOptions, requiredParams);




const client = await getClient(finalOptions.token, true);
const command = new DeleteFileCommand(finalOptions);
return await client.send(command);
}

export async function GetReleaseAction(paramsFile, options){
let finalOptions = {};
const requiredParams = ["releaseId","organisation","application","token"];
Expand Down
Empty file modified airborne-core-cli/bin.js
100755 → 100644
Empty file.
79 changes: 78 additions & 1 deletion airborne-core-cli/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Command } from "commander";
import path from "path";
import { CreateApplicationAction, CreateDimensionAction, CreateFileAction, CreateOrganisationAction, CreatePackageAction, CreateReleaseAction, DeleteDimensionAction, GetReleaseAction, GetUserAction, ListDimensionsAction, ListFileGroupsAction, ListFilesAction, ListOrganisationsAction, ListPackagesAction, ListReleasesAction, PostLoginAction, RequestOrganisationAction, ServeReleaseAction, ServeReleaseV2Action, UpdateDimensionAction, UpdateFileAction, UploadFileAction } from "./action.js";
import { CreateApplicationAction, CreateDimensionAction, CreateFileAction, CreateOrganisationAction, CreatePackageAction, CreateReleaseAction, DeleteDimensionAction, DeleteFileAction, GetReleaseAction, GetUserAction, ListDimensionsAction, ListFileGroupsAction, ListFilesAction, ListOrganisationsAction, ListPackagesAction, ListReleasesAction, PostLoginAction, RequestOrganisationAction, ServeReleaseAction, ServeReleaseV2Action, UpdateDimensionAction, UpdateFileAction, UploadFileAction } from "./action.js";
import { promises as fsPromises } from "fs";
import fs from "fs";
import { fileURLToPath } from 'url';
Expand Down Expand Up @@ -657,6 +657,83 @@ JSON file format (params.json):
});


program
.command("DeleteFile")
.argument('[params_file]', 'JSON file containing all parameters (use @params.json format)')
.option("--file_id <file_id>", "file_id parameter")
.option("--delete_all_versions <delete_all_versions>", "delete_all_versions parameter", (value) => {
if (value.toLowerCase() === 'true') return true;
if (value.toLowerCase() === 'false') return false;
throw new Error("--delete_all_versions must be true or false");
})
.option("--organisation <organisation>", "organisation parameter")
.option("--application <application>", "application parameter")
.option("--token <token>", "Bearer token for authentication")
.description(`
Delete file request operation:

Usage 1 - Individual options:
$ airborne-core-cli DeleteFile \\
--file_id <file_id> \\
--organisation <organisation> \\
--application <application> \\
--token <string> \\
[--delete_all_versions <delete_all_versions>]

Usage 2 - JSON file:
airborne-core-cli DeleteFile @file.json

Usage 3 - Mixed Usage:
$ airborne-core-cli DeleteFile @params.json --file_id <value> --delete_all_versions <value> --token <value>

Parameters:
--file_id <string> (required) : File key in the format "$file_path@version:$version_number" or "$file_path@tag:$tag"
--delete_all_versions <boolean> (optional) : Whether to delete all versions of the file
--organisation <string> (required) : Name of the organisation
--application <string> (required) : Name of the application
--token <string> (required) : Bearer token for authentication

`)
.usage('<action> [options]')
.addHelpText('after', `
Examples:

1. Using individual options:
$ airborne-core-cli DeleteFile \\
--file_id <file_id> \\
--organisation <organisation> \\
--application <application> \\
--token <string> \\
[--delete_all_versions <delete_all_versions>]

2. Using JSON file:
$ airborne-core-cli DeleteFile @params.json

3. Mixed approach (JSON file + CLI overrides):
$ airborne-core-cli DeleteFile @params.json --file_id <value> --delete_all_versions <value> --token <value>

JSON file format (params.json):
{
"file_id": "example_file_id",
"delete_all_versions": "example_delete_all_versions",
"organisation": "example_organisation",
"application": "example_application",
"token": "your_bearer_token_here"
}`)
.action(async (paramsFile, options) => {
try {

const output = await DeleteFileAction(paramsFile, options);
console.log(printColoredJSON(output));
process.exit(0);
} catch (err) {
console.error("Error message:", err.message);
console.error("Error executing:", printColoredJSON(err));
process.exit(1);
}
});


program
.command("GetRelease")
.argument('[params_file]', 'JSON file containing all parameters (use @params.json format)')
Expand Down
80 changes: 61 additions & 19 deletions airborne_dashboard/app/dashboard/[orgId]/[appId]/files/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Search, ChevronDown, ChevronRight, File, Filter, Plus, Loader2, Pencil } from "lucide-react";
import { FileCreationModal } from "@/components/file-creation-modal";
import { useAppContext } from "@/providers/app-context";
import { apiFetch } from "@/lib/api";
import { hasAppAccess } from "@/lib/utils";
import { useDebouncedValue } from "@/hooks/useDebouncedValue";
import { Search, ChevronDown, ChevronRight, File, Filter, Plus, Loader2, Pencil, Trash2 } from "lucide-react";
import { toastSuccess, toastError } from "@/hooks/use-toast";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import {
Expand All @@ -34,8 +29,19 @@ import {
PaginationEllipsis,
} from "@/components/ui/pagination";
import type { FileGroup, FileGroupsResponse, TagInfo, TagsResponse } from "@/types/files";
import { FileCreationModal } from "@/components/file-creation-modal";
import { FileDeleteModal } from "@/components/file-delete-modal";
import { useAppContext } from "@/providers/app-context";
import { apiFetch } from "@/lib/api";
import { hasAppAccess } from "@/lib/utils";
import { useDebouncedValue } from "@/hooks/useDebouncedValue";

const FILES_PER_PAGE = 15;
type FileToDelete = {
id: string;
file_path: string;
version: number;
};

export default function FilesPage() {
const { token, org, app, getOrgAccess, getAppAccess } = useAppContext();
Expand All @@ -51,6 +57,8 @@ export default function FilesPage() {
// UI state
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [expandedGroup, setExpandedGroup] = useState<string | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [fileToDelete, setFileToDelete] = useState<FileToDelete | null>(null);

// Edit Tag dialog state
const [isEditTagDialogOpen, setIsEditTagDialogOpen] = useState(false);
Expand Down Expand Up @@ -95,6 +103,15 @@ export default function FilesPage() {
const totalPages = groupsData?.total_pages || 1;
const tags = tagsData?.data || [];

function handleDeleteClick(filePath: string, version: number) {
setFileToDelete({
id: `${filePath}@version:${version}`,
file_path: filePath,
version,
});
setIsDeleteModalOpen(true);
}

const handleSearchChange = (value: string) => {
setSearchQuery(value);
setPage(1);
Expand Down Expand Up @@ -395,7 +412,7 @@ export default function FilesPage() {
<TableHead>URL</TableHead>
<TableHead>Size</TableHead>
<TableHead>Created</TableHead>
<TableHead className="w-24">Actions</TableHead>
<TableHead className="w-52">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
Expand Down Expand Up @@ -424,18 +441,32 @@ export default function FilesPage() {
</TableCell>
<TableCell>
{hasAppAccess(getOrgAccess(org), getAppAccess(org, app)) && (
<Button
variant="ghost"
size="sm"
className="h-7 px-2"
onClick={(e) => {
e.stopPropagation();
handleEditTag(group.file_path, version.version, versionTag || "");
}}
>
<Pencil className="h-3.5 w-3.5 mr-1" />
Edit Tag
</Button>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
className="h-7 px-2"
onClick={(e) => {
e.stopPropagation();
handleEditTag(group.file_path, version.version, versionTag || "");
}}
>
<Pencil className="h-3.5 w-3.5 mr-1" />
Edit Tag
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-destructive hover:text-destructive"
onClick={(e) => {
e.stopPropagation();
handleDeleteClick(group.file_path, version.version);
}}
>
<Trash2 className="h-3.5 w-3.5 mr-1" />
Delete
</Button>
</div>
)}
</TableCell>
</TableRow>
Expand Down Expand Up @@ -521,6 +552,17 @@ export default function FilesPage() {
</DialogFooter>
</DialogContent>
</Dialog>
<FileDeleteModal
open={isDeleteModalOpen}
onOpenChange={(open) => {
setIsDeleteModalOpen(open);
if (!open) {
setFileToDelete(null);
}
}}
file={fileToDelete}
onDeleted={() => mutate()}
/>
</div>
);
}
123 changes: 123 additions & 0 deletions airborne_dashboard/components/file-delete-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"use client";

import { useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { useAppContext } from "@/providers/app-context";
import { apiFetch } from "@/lib/api";
import { Trash2 } from "lucide-react";

interface FileDeleteModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
file: {
id: string;
file_path: string;
version: number;
} | null;
onDeleted?: () => void;
}

export function FileDeleteModal({ open, onOpenChange, file, onDeleted }: FileDeleteModalProps) {
const { token, org, app } = useAppContext();
const [deleteAllVersions, setDeleteAllVersions] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleDelete = async () => {
if (!file) return;

setIsDeleting(true);
setError(null);

try {
await apiFetch(
"/file",
{
method: "DELETE",
query: {
file_id: file.id,
delete_all_versions: deleteAllVersions,
},
},
{ token, org, app }
);
onOpenChange(false);
setDeleteAllVersions(false);
onDeleted?.();
} catch (error: unknown) {
setError(error instanceof Error ? error.message : "Failed to delete file");
} finally {
setIsDeleting(false);
}
};

const handleClose = () => {
if (!isDeleting) {
onOpenChange(false);
setDeleteAllVersions(false);
setError(null);
}
};

if (!file) return null;

return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 font-[family-name:var(--font-space-grotesk)]">
<Trash2 className="h-5 w-5 text-red-500" />
Delete File
</DialogTitle>
<DialogDescription>
Are you sure you want to delete{" "}
<code className="rounded bg-muted px-1 py-0.5 text-sm font-mono">{file.file_path}</code>?
<br />
<span className="text-red-600">This action cannot be undone.</span>
</DialogDescription>
</DialogHeader>

<div className="space-y-4 py-4">
<div className="flex items-start space-x-3 rounded-lg border p-4">
<Checkbox
id="deleteAllVersions"
checked={deleteAllVersions}
onCheckedChange={(checked) => setDeleteAllVersions(checked === true)}
className="mt-0.5"
/>
<div className="space-y-1">
<Label htmlFor="deleteAllVersions" className="text-sm font-medium leading-none cursor-pointer">
Delete all versions
</Label>
<p className="text-xs text-muted-foreground">
When checked, all versions of this file will be deleted. Otherwise, only version {file.version} will be
removed.
</p>
</div>
</div>

{error && <div className="rounded-lg bg-red-50 p-3 text-sm text-red-600 dark:bg-red-950/50">{error}</div>}
</div>

<DialogFooter>
<Button variant="outline" onClick={handleClose} disabled={isDeleting}>
Cancel
</Button>
<Button variant="destructive" onClick={handleDelete} disabled={isDeleting} className="gap-2">
{isDeleting ? "Deleting..." : "Delete File"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Remove status column and enum type from files table
ALTER TABLE hyperotaserver.files DROP COLUMN status;
DROP TYPE IF EXISTS hyperotaserver.file_status;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TYPE hyperotaserver.file_status AS ENUM ('pending', 'ready', 'deleted');

ALTER TABLE hyperotaserver.files
ADD COLUMN status hyperotaserver.file_status NOT NULL DEFAULT 'pending';

-- Backfill existing rows: files with size > 0 are ready, rest are pending
UPDATE hyperotaserver.files SET status = 'ready' WHERE size > 0;
UPDATE hyperotaserver.files SET status = 'pending' WHERE size = 0;
Loading
Loading