Skip to content
Closed
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
1,224 changes: 1,178 additions & 46 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"preview": "vite preview"
},
"dependencies": {
"@h5web/app": "^16.0.1",
"@h5web/lib": "^16.0.1",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-icons": "^1.3.2",
Expand Down
93 changes: 93 additions & 0 deletions src/DialogComp/NexusPreviewDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from "react";
import * as Dialog from "@radix-ui/react-dialog";
import { App, H5GroveProvider } from "@h5web/app";
import "@h5web/app/dist/styles.css";
import config from "@/utils/config";

const apiUrl = config.apiUrl;


interface NexusPreviewDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
datasetId: string | null;
onDownload?: () => void;
}

export function NexusPreviewDialog({
open,
onOpenChange,
datasetId,
onDownload,
}: NexusPreviewDialogProps) {
if (!datasetId) return null;

const urlObj = new URL(apiUrl);
urlObj.pathname = urlObj.pathname.replace(/\/template$/, '/h5grove');
const hdf5Url = urlObj.toString()
console.log(hdf5Url)
const filePath = `${datasetId}.nxs`; // relative filename inside NEXUS_DIR


const handleDownload = () => {
const link = document.createElement("a");
link.href = hdf5Url;
link.download = `${datasetId}.nxs`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);

if (onDownload) {
onDownload();
}
};

return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50 z-40" />
<Dialog.Content
className="fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]
w-[90vw] max-w-[1200px] h-[85vh] bg-white rounded-lg shadow-xl
border border-gray-200 p-6 z-50 focus:outline-none"
>
<div className="flex flex-col h-full">
<div className="mb-4">
<Dialog.Title className="text-2xl font-semibold text-gray-900">
NeXus File Preview
</Dialog.Title>
<Dialog.Description className="text-sm text-gray-600 mt-1">
Explore the NeXus file structure and data using the interactive viewer below.
</Dialog.Description>
</div>

<div className="flex-1 border border-gray-300 rounded-md overflow-hidden">
<H5GroveProvider url={hdf5Url} filepath={filePath}>
<App />
</H5GroveProvider>
</div>

<div className="flex justify-end gap-3 mt-4">
<button
onClick={() => onOpenChange(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border
border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none
focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Close
</button>
<button
onClick={handleDownload}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border
border-transparent rounded-md hover:bg-blue-700 focus:outline-none
focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Download NeXus File
</button>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
19 changes: 19 additions & 0 deletions src/IconsComponents/NexusIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";

export const NexusIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
<polyline points="7.5 10 12 13 16.5 10" />
<line x1="12" y1="13" x2="12" y2="19" />
</svg>
);
30 changes: 30 additions & 0 deletions src/MenuBar/Header.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
margin-left: 1rem;
font-size: 16px;
}

.topMenuBar {
/* flex items-center gap-3 px-3 py-2 */
display: flex;
Expand All @@ -64,6 +65,7 @@
color: #282828;
text-transform: uppercase;
}

.uuid {
color: #282828;
margin: 0;
Expand All @@ -72,6 +74,7 @@
.name {
margin: 0.6rem;
}

.saveUuid {
display: flex;
gap: 16px;
Expand All @@ -83,3 +86,30 @@
color: #ea5252;
font-weight: 500;
}

.upload-link {
padding: 0.5rem 1rem;
background-color: #667eea;
color: white;
text-decoration: none;
border-radius: 6px;
font-weight: 600;
font-size: 14px;
transition: background-color 0.3s ease;
}

.upload-link:hover {
background-color: #5568d3;
}

.projectName {
display: flex;
gap: 0.5rem;
align-items: center;
font-size: 14px;
}

.projectLabel {
font-weight: 600;
color: #666;
}
3 changes: 3 additions & 0 deletions src/MenuBar/LogoBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export default function LogoBar({ startScreen }) {
) : (
<p className="projectPromt">Project is not selected</p>
)}
<Link to="/upload" className="upload-link">
Upload Excel
</Link>
<PreferencesDialog
setProjectName={setProjectName}
projectName={projectName}
Expand Down
43 changes: 42 additions & 1 deletion src/StartScreenComp/StartScreenComp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ import {

import { downloadFile } from "../lib/request";
import { useLocalStorage } from "./hooks/useLocalStorage";
import { useNexusPreview } from "./hooks/useNexusPreview";
import { NexusPreviewDialog } from "../DialogComp/NexusPreviewDialog";

import UnderDev from "@/ui/UnderDev";
import DescriptionComp from "./DescriptionComp";
import "./StartScreenComp.css";
import Tabs from "./Tabs";

export default function StartScreenComp({}) {
export default function StartScreenComp({ }) {
const [value, setValue] = useState("");
const [copied, setCopied] = useState(false);

Expand Down Expand Up @@ -106,6 +108,18 @@ export default function StartScreenComp({}) {
const onChange = (e) => {
setValue(e.target.value);
};

// NeXus preview hook
const {
isGenerating,
showPreview,
datasetId,
error: nexusError,
generateAndPreview,
downloadNexus,
closePreview,
} = useNexusPreview({ templateId: idShosen, projectID });

const storageItemKey = "my-survey";
return (
<div className="screenWrap">
Expand Down Expand Up @@ -184,9 +198,36 @@ export default function StartScreenComp({}) {
<Link to={`/wizard/${idShosen}`}>
<Button disabled={!idShosen} label="Customize Excel template" />
</Link>
<div onClick={downloadNexus}>
<Button
disabled={!idShosen}
label="Download NeXus File"
/>
</div>
<div onClick={generateAndPreview}>
<Button
disabled={!idShosen || isGenerating}
label={isGenerating ? "Generating preview..." : "Preview NeXus File"}
/>
</div>
</div>
</div>
</div>

{/* NeXus Preview Dialog */}
<NexusPreviewDialog
open={showPreview}
onOpenChange={closePreview}
datasetId={datasetId}
onDownload={downloadNexus}
/>

{/* Error notification for NeXus */}
{nexusError && (
<Notification mode="error">
{nexusError}
</Notification>
)}
</div>
);
}
110 changes: 110 additions & 0 deletions src/StartScreenComp/hooks/useNexusPreview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { useState } from "react";
import config from "@/utils/config";
import { downloadFile } from "@/lib/request";

const apiUrl = config.apiUrl;

interface UseNexusPreviewProps {
templateId: string | null;
projectID?: string;
}

interface UseNexusPreviewReturn {
isGenerating: boolean;
showPreview: boolean;
datasetId: string | null;
error: string | null;
generateAndPreview: () => Promise<void>;
downloadNexus: () => void;
closePreview: () => void;
}

export const useNexusPreview = ({
templateId,
projectID,
}: UseNexusPreviewProps): UseNexusPreviewReturn => {
const [isGenerating, setIsGenerating] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [datasetId, setDatasetId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);

const generateAndPreview = async () => {
if (!templateId) {
setError("No template selected");
return;
}

setIsGenerating(true);
setError(null);

try {
// Step 1: Generate NeXus file from template
const nexusUrl = `${apiUrl}/template/${templateId}?format=nxs${projectID ? `&project=${projectID}` : ""
}`;

const response = await fetch(nexusUrl);
if (!response.ok) {
throw new Error(`Failed to generate NeXus file: ${response.statusText}`);
}

const nexusBlob = await response.blob();

// Step 2: Upload to /dataset endpoint for preview
const formData = new FormData();
formData.append("file", nexusBlob, `${templateId}.nxs`);

const uploadResponse = await fetch(`${apiUrl}/dataset?dataset_type=ambit_json`, {
method: "POST",
body: formData,
});

if (!uploadResponse.ok) {
throw new Error(`Failed to upload NeXus file: ${uploadResponse.statusText}`);
}

const uploadResult = await uploadResponse.json();

// Extract dataset UUID from task result
const task = uploadResult.task?.[0];
if (task && task.result_uuid) {
setDatasetId(task.result_uuid);
setShowPreview(true);
} else {
throw new Error("No dataset ID returned from upload");
}
} catch (err) {
console.error("Error generating/uploading NeXus file:", err);
setError(err instanceof Error ? err.message : "Unknown error occurred");
} finally {
setIsGenerating(false);
}
};

const downloadNexus = () => {
if (!templateId) {
console.info("No template chosen, skipping download.");
return;
}

const nexusUrl = `${apiUrl}/template/${templateId}?format=nxs${projectID ? `&project=${projectID}` : ""
}`;

downloadFile(templateId, nexusUrl);
};

const closePreview = () => {
setShowPreview(false);
setDatasetId(null);
setError(null);
};

return {
isGenerating,
showPreview,
datasetId,
error,
generateAndPreview,
downloadNexus,
closePreview,
};
};
Loading
Loading