mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-12-17 16:29:54 +00:00
feat(UI): Adding Files API to Admin UI (#4319)
# What does this PR do? ## Files Admin Page <img width="1919" height="1238" alt="Screenshot 2025-12-09 at 10 33 06 AM" src="https://github.com/user-attachments/assets/3dd545f0-32bc-45be-af2b-1823800015f2" /> ## Files Upload Modal <img width="1919" height="1287" alt="Screenshot 2025-12-09 at 10 33 38 AM" src="https://github.com/user-attachments/assets/776bb372-75d3-4ccd-b6b5-c9dfb3fcb350" /> ## Files Detail <img width="1918" height="1099" alt="Screenshot 2025-12-09 at 10 34 26 AM" src="https://github.com/user-attachments/assets/f256dbf8-4047-4d79-923d-404161b05f36" /> Note, content preview has some handling for JSON, CSV, and PDF to enable nicer rendering. Pure text rendering is trivial. ### Files Detail File Content Preview (TXT) <img width="1918" height="1341" alt="Screenshot 2025-12-09 at 10 41 20 AM" src="https://github.com/user-attachments/assets/4fa0ddb7-ffff-424b-b764-0bd4af6ed976" /> ### Files Detail File Content Preview (JSON) <img width="1909" height="1233" alt="Screenshot 2025-12-09 at 10 39 57 AM" src="https://github.com/user-attachments/assets/b912f07a-2dff-483b-b73c-2f69dd0d87ad" /> ### Files Detail File Content Preview (HTML) <img width="1916" height="1348" alt="Screenshot 2025-12-09 at 10 40 27 AM" src="https://github.com/user-attachments/assets/17ebec0a-8754-4552-977d-d3c44f7f6973" /> ### Files Detail File Content Preview (CSV) <img width="1919" height="1177" alt="Screenshot 2025-12-09 at 10 34 50 AM" src="https://github.com/user-attachments/assets/20bd0755-1757-4a3a-99d2-fbd072f81f49" /> ### Files Detail File Content Preview (PDF) <img width="1917" height="1154" alt="Screenshot 2025-12-09 at 10 36 48 AM" src="https://github.com/user-attachments/assets/2873e6fe-4da3-4cbd-941b-7d903270b749" /> Closes https://github.com/llamastack/llama-stack/issues/4144 ## Test Plan Added Tests Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
This commit is contained in:
parent
6ad5fb5577
commit
fcea9893a4
23 changed files with 4034 additions and 23 deletions
205
src/llama_stack_ui/lib/file-utils.test.ts
Normal file
205
src/llama_stack_ui/lib/file-utils.test.ts
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import {
|
||||
formatFileSize,
|
||||
getFileTypeIcon,
|
||||
formatPurpose,
|
||||
getPurposeDescription,
|
||||
formatTimestamp,
|
||||
truncateFilename,
|
||||
isTextFile,
|
||||
createDownloadUrl,
|
||||
} from "./file-utils";
|
||||
|
||||
describe("file-utils", () => {
|
||||
describe("formatFileSize", () => {
|
||||
test("formats bytes correctly", () => {
|
||||
expect(formatFileSize(0)).toBe("0 B");
|
||||
expect(formatFileSize(500)).toBe("500 B");
|
||||
expect(formatFileSize(1023)).toBe("1023 B");
|
||||
});
|
||||
|
||||
test("formats kilobytes correctly", () => {
|
||||
expect(formatFileSize(1024)).toBe("1.0 KB");
|
||||
expect(formatFileSize(1536)).toBe("1.5 KB");
|
||||
expect(formatFileSize(1024 * 1023)).toBe("1023.0 KB");
|
||||
});
|
||||
|
||||
test("formats megabytes correctly", () => {
|
||||
expect(formatFileSize(1024 * 1024)).toBe("1.0 MB");
|
||||
expect(formatFileSize(1024 * 1024 * 2.5)).toBe("2.5 MB");
|
||||
});
|
||||
|
||||
test("formats gigabytes correctly", () => {
|
||||
expect(formatFileSize(1024 * 1024 * 1024)).toBe("1.0 GB");
|
||||
expect(formatFileSize(1024 * 1024 * 1024 * 1.8)).toBe("1.8 GB");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFileTypeIcon", () => {
|
||||
test("returns correct icons for common file types", () => {
|
||||
expect(getFileTypeIcon("pdf")).toBe("📕");
|
||||
expect(getFileTypeIcon(".pdf")).toBe("📕");
|
||||
expect(getFileTypeIcon("txt")).toBe("📄");
|
||||
expect(getFileTypeIcon("html")).toBe("🌐");
|
||||
expect(getFileTypeIcon("md")).toBe("📝");
|
||||
expect(getFileTypeIcon("csv")).toBe("📊");
|
||||
expect(getFileTypeIcon("json")).toBe("⚙️");
|
||||
expect(getFileTypeIcon("docx")).toBe("📘");
|
||||
expect(getFileTypeIcon("js")).toBe("⚡");
|
||||
expect(getFileTypeIcon("py")).toBe("🐍");
|
||||
});
|
||||
|
||||
test("handles MIME types", () => {
|
||||
expect(getFileTypeIcon("application/pdf")).toBe("📕");
|
||||
expect(getFileTypeIcon("text/plain")).toBe("📄");
|
||||
expect(getFileTypeIcon("text/markdown")).toBe("📝");
|
||||
expect(getFileTypeIcon("application/json")).toBe("⚙️");
|
||||
expect(getFileTypeIcon("text/html")).toBe("🌐");
|
||||
});
|
||||
|
||||
test("returns default icon for unknown types", () => {
|
||||
expect(getFileTypeIcon("unknown")).toBe("📄");
|
||||
expect(getFileTypeIcon("")).toBe("📄");
|
||||
expect(getFileTypeIcon(undefined)).toBe("📄");
|
||||
});
|
||||
|
||||
test("handles complex MIME types", () => {
|
||||
expect(
|
||||
getFileTypeIcon(
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
)
|
||||
).toBe("📘");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatPurpose", () => {
|
||||
test("formats purpose labels correctly", () => {
|
||||
expect(formatPurpose("fine-tune")).toBe("Fine-tuning");
|
||||
expect(formatPurpose("assistants")).toBe("Assistants");
|
||||
expect(formatPurpose("user_data")).toBe("User Data");
|
||||
expect(formatPurpose("batch")).toBe("Batch Processing");
|
||||
expect(formatPurpose("vision")).toBe("Vision");
|
||||
expect(formatPurpose("evals")).toBe("Evaluations");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPurposeDescription", () => {
|
||||
test("returns correct descriptions", () => {
|
||||
expect(getPurposeDescription("fine-tune")).toBe(
|
||||
"For training and fine-tuning language models"
|
||||
);
|
||||
expect(getPurposeDescription("assistants")).toBe(
|
||||
"For use with AI assistants and chat completions"
|
||||
);
|
||||
expect(getPurposeDescription("user_data")).toBe(
|
||||
"General user data and documents"
|
||||
);
|
||||
expect(getPurposeDescription("batch")).toBe(
|
||||
"For batch processing and bulk operations"
|
||||
);
|
||||
expect(getPurposeDescription("vision")).toBe(
|
||||
"For computer vision and image processing tasks"
|
||||
);
|
||||
expect(getPurposeDescription("evals")).toBe(
|
||||
"For model evaluation and testing"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatTimestamp", () => {
|
||||
test("formats Unix timestamp to readable date", () => {
|
||||
const timestamp = 1640995200; // Jan 1, 2022 00:00:00 UTC
|
||||
const result = formatTimestamp(timestamp);
|
||||
|
||||
// Should return a valid date string
|
||||
expect(typeof result).toBe("string");
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
|
||||
// Test that it's calling Date correctly by using a known timestamp
|
||||
const testDate = new Date(timestamp * 1000);
|
||||
expect(result).toBe(testDate.toLocaleString());
|
||||
});
|
||||
});
|
||||
|
||||
describe("truncateFilename", () => {
|
||||
test("returns filename unchanged if under max length", () => {
|
||||
expect(truncateFilename("short.txt", 30)).toBe("short.txt");
|
||||
});
|
||||
|
||||
test("truncates long filenames while preserving extension", () => {
|
||||
const longFilename =
|
||||
"this_is_a_very_long_filename_that_should_be_truncated.pdf";
|
||||
const result = truncateFilename(longFilename, 30);
|
||||
|
||||
expect(result).toContain("...");
|
||||
expect(result).toContain(".pdf");
|
||||
expect(result.length).toBeLessThanOrEqual(30);
|
||||
});
|
||||
|
||||
test("handles files without extension", () => {
|
||||
const longFilename = "this_is_a_very_long_filename_without_extension";
|
||||
const result = truncateFilename(longFilename, 20);
|
||||
|
||||
expect(result).toContain("...");
|
||||
expect(result.length).toBeLessThanOrEqual(20);
|
||||
});
|
||||
|
||||
test("uses default max length", () => {
|
||||
const longFilename = "a".repeat(50) + ".txt";
|
||||
const result = truncateFilename(longFilename);
|
||||
|
||||
expect(result).toContain("...");
|
||||
expect(result).toContain(".txt");
|
||||
expect(result.length).toBeLessThanOrEqual(30); // Default max length
|
||||
});
|
||||
});
|
||||
|
||||
describe("isTextFile", () => {
|
||||
test("identifies text files correctly", () => {
|
||||
expect(isTextFile("text/plain")).toBe(true);
|
||||
expect(isTextFile("text/html")).toBe(true);
|
||||
expect(isTextFile("text/markdown")).toBe(true);
|
||||
expect(isTextFile("application/json")).toBe(true);
|
||||
});
|
||||
|
||||
test("identifies non-text files correctly", () => {
|
||||
expect(isTextFile("application/pdf")).toBe(false);
|
||||
expect(isTextFile("image/png")).toBe(false);
|
||||
expect(isTextFile("application/octet-stream")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createDownloadUrl", () => {
|
||||
// Mock URL.createObjectURL and revokeObjectURL for Node environment
|
||||
const mockCreateObjectURL = jest.fn(() => "blob:mock-url");
|
||||
const mockRevokeObjectURL = jest.fn();
|
||||
|
||||
beforeAll(() => {
|
||||
global.URL = {
|
||||
...global.URL,
|
||||
createObjectURL: mockCreateObjectURL,
|
||||
revokeObjectURL: mockRevokeObjectURL,
|
||||
} as typeof URL;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockCreateObjectURL.mockClear();
|
||||
mockRevokeObjectURL.mockClear();
|
||||
});
|
||||
|
||||
test("creates download URL from string content", () => {
|
||||
const content = "Hello, world!";
|
||||
const url = createDownloadUrl(content);
|
||||
|
||||
expect(url).toBe("blob:mock-url");
|
||||
expect(mockCreateObjectURL).toHaveBeenCalledWith(expect.any(Blob));
|
||||
});
|
||||
|
||||
test("creates download URL from Blob", () => {
|
||||
const blob = new Blob(["Hello, world!"], { type: "text/plain" });
|
||||
const url = createDownloadUrl(blob);
|
||||
|
||||
expect(url).toBe("blob:mock-url");
|
||||
expect(mockCreateObjectURL).toHaveBeenCalledWith(blob);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue