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
4 changes: 2 additions & 2 deletions backend/sbs_server/app/route.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def xdc_run(files):
return jsonify({"error": "No selected Params file"}), 400
params_from_request = json.loads(params_file.read())

required_params = ['sbh_url', 'sbh_token', 'fj_url', 'fj_token', 'collection_url', 'fj_study_id',
required_params = ['sbh_url', 'sbh_prefix', 'sbh_token', 'fj_url', 'fj_token', 'collection_url', 'fj_study_id',
'sbh_overwrite', 'fj_overwrite', 'importType']

for param in required_params:
Expand Down Expand Up @@ -180,7 +180,7 @@ def xdc_run(files):
if isinstance(tl, sbol2.Experiment):
experimentId = tl.displayId
break
upload_sbh_attachments(sbh_url, sbh_token, sbh_user, sbol_graph_uri, sbh_collection_url, attachments, experimentId)
upload_sbh_attachments(sbh_url, sbh_prefix, sbh_token, sbh_user, sbol_graph_uri, sbh_collection_url, attachments, experimentId)
except Exception as e:
print('Error uploading attachments to SynBioHub')
return jsonify({"error": f"Error uploading attachments to SynBioHub: {e}"}), 400
Expand Down
4 changes: 3 additions & 1 deletion backend/sbs_server/app/synbiohubUpload.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import requests
import os

def upload_sbh_attachments(sbh_url, sbh_token, sbh_user, sbh_user_graph, sbh_collection_url, attachments, experimentId=None):
def upload_sbh_attachments(sbh_url, sbh_prefix, sbh_token, sbh_user, sbh_user_graph, sbh_collection_url, attachments, experimentId=None):
headers = {'Accept': 'text/plain', 'X-authorization': sbh_token}

for attachment_name, file in attachments.items():
Expand All @@ -15,6 +15,8 @@ def upload_sbh_attachments(sbh_url, sbh_token, sbh_user, sbh_user_graph, sbh_col
search_result = sbh_get_attachment_uri(sbh_url, sbh_token, sbh_user_graph, sbh_collection_url, resolved_name)
for binding in search_result["results"]["bindings"]:
uri = binding["s"]["value"]
if sbh_prefix:
uri = uri.replace(sbh_prefix,sbh_url)
response = requests.get(f'{uri}/remove', headers=headers)
if not response.ok:
raise Exception(f"Deleting existing attachment failed ({response.status_code}): {response.text}")
Expand Down
1 change: 0 additions & 1 deletion frontend/src/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,6 @@ export async function uploadExperiment(
),
...(extraFiles.sheetName ? { sheet_name: extraFiles.sheetName } : {})
}
console.log("uploadExperiment paramsObj:", paramsObj);

const paramsJson = JSON.stringify(paramsObj);
const paramBlob = new Blob([paramsJson], { type: 'application/json' });
Expand Down
115 changes: 95 additions & 20 deletions frontend/src/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { loadOverlay, closeOverlay } from "./redux/slices/loadingOverlay"
import { MODAL_TYPES } from "./modules/unified_modal/unifiedModal"
import { upload_resource, upload_sbol, CheckLogin } from "./API"
import { readStudy } from "./modules/util";
import { workingDirectorySlice } from './redux/store'

const EXCEL_VIEWER_PANEL_TYPE = 'synbio.panel-type.excel-viewer'

Expand All @@ -29,32 +30,68 @@ export default {
const file = findFileByNameOrId(fileNameOrId)
if (!file)
return "File doesn't exist."

const dirHandle = store.getState().workingDirectory.directoryHandle
const directory = file.id.split("/")[0]

try {
const tempDirectory = await dirHandle.getDirectoryHandle(directory);
const dirHandle =
store.getState().workingDirectory.directoryHandle

// Resolve the directory containing the file
const parts = file.id.split('/')
const fileName = parts.pop()

let currentDir = dirHandle

for (const part of parts) {
currentDir = await currentDir.getDirectoryHandle(part)
}

// If this is a JSON workflow file, preserve your existing
// behavior of finding an uploaded file referenced by it.
let uploadedFilePath = null

let uploadedFilePath = null;
if (fileName.toLowerCase().endsWith('.json')) {
try {
const jsonFH = await tempDirectory.getFileHandle(file.name);
const jsonText = await (await jsonFH.getFile()).text();
const jsonData = JSON.parse(jsonText);
uploadedFilePath = jsonData.file || null;
} catch (e) {}
const jsonFH = await currentDir.getFileHandle(fileName)
const jsonText = await (await jsonFH.getFile()).text()
const jsonData = JSON.parse(jsonText)

await tempDirectory.removeEntry(file.name);
uploadedFilePath = jsonData.file || null
} catch (e) {
// Not a readable workflow JSON file
}
}

// Delete the selected file
await currentDir.removeEntry(fileName)

// If deleting an XML source, also delete its upload sidecar.
if (fileName.toLowerCase().endsWith('.xml')) {
const sidecarName = fileName.replace(/\.xml$/i, '.json')
const sidecarId = [...parts, sidecarName].join('/')

try {
if (uploadedFilePath) {
const uploadsDir = await tempDirectory.getDirectoryHandle('uploads');
const uploadFileName = uploadedFilePath.split('/').pop();
await uploadsDir.removeEntry(uploadFileName);
await currentDir.removeEntry(sidecarName)
} catch (e) {
if (e.name !== 'NotFoundError') {
console.warn(`Could not delete sidecar ${sidecarId}:`, e)
}
} catch (e) {}
} catch {
await dirHandle?.removeEntry(file.name);
}

store.dispatch(workDirActions.removeFile(sidecarId))
}

// Preserve your existing uploads/ cleanup.
if (uploadedFilePath) {
try {
const uploadsDir =
await currentDir.getDirectoryHandle('uploads')

const uploadFileName =
uploadedFilePath.split('/').pop()

await uploadsDir.removeEntry(uploadFileName)
} catch (e) {
// Uploaded file may already be gone.
}
}

store.dispatch(panelsActions.closePanel(file.id))
Expand Down Expand Up @@ -350,6 +387,7 @@ export default {
return "Panel isn't open."

await writeToFileHandle(file, serializePanel(file.id))
store.dispatch(workingDirectorySlice.actions.uploadChanged())

if (file.objectType === ObjectTypes.SBOL.id) {
const panel = panelsSelectors.selectById(store.getState(), file.id)
Expand All @@ -363,7 +401,6 @@ export default {
const dirHandle = store.getState().workingDirectory.directoryHandle
sbmlFile = await createFileInDirectory(dirHandle, sbmlFileName, ObjectTypes.SBML.id, store.dispatch)
}

await writeToFileHandle(sbmlFile, sbmlContent)
}
}
Expand Down Expand Up @@ -487,6 +524,7 @@ export default {
const selectedRepo = jsonData.registryURL;
const expectedEmail = jsonData.userEmail || null;
const collectionUrl = jsonData.collectionUri;
const collectionId = jsonData.collectionId;
const collectionName = jsonData.name;
const registryAPI = jsonData.registryAPI;
const importType = directory.endsWith(".xml")?"designs":directory;
Expand All @@ -508,6 +546,43 @@ export default {
store.dispatch(closeOverlay());
}

const collectionEntry = {
name: collectionName,
displayId: collectionId,
uri: collectionUrl,
selectedRepo,
userEmail: expectedEmail
}

const uploadEntry = {
collectionName,
collectionUri: collectionUrl,
uri: collectionUrl,
file: file.id,
date: new Date().toLocaleString(undefined, { timeZoneName: 'short' }),
selectedRepo,
userEmail: expectedEmail,
type: 'upload',
};

const updatedJson = {
file: file.id,
collection: collectionEntry,
uploads: [uploadEntry]
};

const jsonPath = file.id.replace(/\.xml$/i, '.json');
const parts = jsonPath.split('/');
const fileName = parts.pop();
let currentDir = dirHandle;
for (const part of parts) {
currentDir = await currentDir.getDirectoryHandle(part);
}
const jsonFH = await currentDir.getFileHandle(fileName, { create: true });
await writeToFileHandle(jsonFH, JSON.stringify(updatedJson));
Comment on lines +581 to +582

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Delete upload sidecars with their XML files

Creating this sibling JSON file introduces persistent state that FileDelete does not remove when the user deletes the XML source. If a design/device is uploaded, deleted, and later recreated with the same filename, ExplorerListItem reads the orphaned sidecar and immediately colors the new, never-uploaded object green; the sidecar should be cleaned up with its XML file or otherwise tied to the source lifecycle.

Useful? React with 👍 / 👎.


store.dispatch(workingDirectorySlice.actions.uploadChanged())

showNotification({
title: "File uploaded",
message: `${file.name} uploaded successfully to ${collectionName} study.`,
Expand Down
20 changes: 15 additions & 5 deletions frontend/src/components/DragObject.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Group, Text } from '@mantine/core'
import React from 'react'

export default function DragObject({ icon, type, title, fileId, ...props }) {
export default function DragObject({ icon, type, title, fileId, uploadInfo, ...props }) {

const handleDragStart = event => {
event.dataTransfer.setData("name", title)
Expand All @@ -17,13 +17,23 @@ export default function DragObject({ icon, type, title, fileId, ...props }) {

return (
<Group
sx={groupStyle}
draggable={true}
onDragStart={handleDragStart}
{...props}
sx={(theme) => ({...groupStyle(theme),
color: uploadInfo!=null?theme.colors.green[6]:undefined,
})}
draggable
onDragStart={handleDragStart}
>
{icon}
<Text size='sm' sx={textStyle}>{title}</Text>
<Text
size="sm"
sx={(theme) => ({
...textStyle(theme),
color: uploadInfo!=null?theme.colors.green[6]:undefined,
})}
>
{title}
</Text>
</Group>
)
}
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/activities/explorer/ExplorerList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export default function ExplorerList({workDir, objectTypesToList}) {
// grab file handles
const files = useFiles()

const filteredFiles = files.filter(file =>
objectTypesToList.includes(file.objectType)
)

const { workflows } = useUnifiedModal()

const [importedFile, setImportedFile] = useState(null)
Expand Down Expand Up @@ -189,7 +193,7 @@ export default function ExplorerList({workDir, objectTypesToList}) {
Object.values(ObjectTypes).map((objectType, i) => {
// grab files of current type
if(objectTypesToList.includes(objectType.id)){
const filesOfType = files.filter(file => file.objectType == objectType.id)
const filesOfType = filteredFiles.filter(file => file.objectType == objectType.id)
.sort((a, b) => a.name?.localeCompare(b.name))
return (
<Accordion.Item value={objectType.id} key={i}>
Expand Down
93 changes: 91 additions & 2 deletions frontend/src/components/activities/explorer/ExplorerListItem.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,106 @@
import commands from "../../../commands"
import { Menu } from '@mantine/core'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { useOpenPanel } from '../../../redux/hooks/panelsHooks'
import { titleFromFileName, useFile } from '../../../redux/hooks/workingDirectoryHooks'
import DragObject from '../../DragObject'
import { getPanelTypeForObject } from '../../../panels'

import store from '../../../redux/store'

export default function ExplorerListItem({ fileId, icon }) {

const file = useFile(fileId)

const [uploadInfo, setUploadInfo] = useState(null)

const uploadRevision = useSelector(state => state.workingDirectory.uploadRevision ?? 0)

useEffect(() => {
const getUploadInfo = async () => {
try {
let jsonFile;
if (file?.objectType === 'synbio.object-type.study-data' ||
file?.objectType === 'synbio.object-type.plate-reader' ||
file?.objectType === 'synbio.object-type.experimental-results') {
const state = store.getState().workingDirectory
const xdcFiles = Object.values(state.entities)
.filter(f => f?.name?.toLowerCase().endsWith('.xdc'))
for (const xdcHandle of xdcFiles) {
try {
const xdcFile = await xdcHandle.getFile()
const xdcText = await xdcFile.text()
const xdc = JSON.parse(xdcText)
const metadataMatches =
file?.objectType === 'synbio.object-type.study-data' &&
(xdc.metadata === file.id ||
xdc.metadata?.split('/').pop() === file.name)
const plateMatches =
file?.objectType === 'synbio.object-type.plate-reader' &&
(xdc.plateOutput === file.id ||
xdc.plateOutput?.split('/').pop() === file.name)
const results = Array.isArray(xdc.results)
? xdc.results
: xdc.results
? [xdc.results]
: []

const resultsMatches = results.some(result =>
file?.objectType === 'synbio.object-type.experimental-results' &&
(result === file.id ||
result.split('/').pop() === file.name)
)
if (!metadataMatches && !plateMatches && !resultsMatches) {
continue
}
if (xdc.uploads.length > 0) {
setUploadInfo(xdc.uploads[xdc.uploads.length - 1])
Comment thread
cjmyers marked this conversation as resolved.
return
}
} catch (error) {
console.warn(`Could not inspect ${xdcHandle.id}:`,error)
}
}
setUploadInfo(null)
return
} else if (file?.name?.toLowerCase().endsWith('.json')||
file?.name?.toLowerCase().endsWith('.xdc')) {
// The selected file is already the JSON metadata file
jsonFile = await file.getFile();
} else if (file?.name?.toLowerCase().endsWith('.xml')) {
const jsonPath = file.id.replace(/\.xml$/i, '.json');
const parts = jsonPath.split('/');
const fileName = parts.pop();
const rootHandle =
store.getState().workingDirectory.directoryHandle;
let currentDir = rootHandle;
for (const part of parts) {
currentDir = await currentDir.getDirectoryHandle(part);
}
const jsonHandle =
await currentDir.getFileHandle(fileName);
jsonFile = await jsonHandle.getFile();
} else {
setUploadInfo(null);
return;
}
const jsonText = await jsonFile.text();
const json = JSON.parse(jsonText);
if (Array.isArray(json.uploads) && json.uploads.length > 0) {
setUploadInfo(json.uploads[json.uploads.length - 1]);
} else {
setUploadInfo(null);
}
} catch (error) {
setUploadInfo(null);
}
};
getUploadInfo();
}, [file?.id, file?.name, uploadRevision]);

// handle opening of file
const openPanel = useOpenPanel()

const handleOpenFile = async () => {
const hasWorkflowPanel = !!getPanelTypeForObject(file)
if (hasWorkflowPanel) {
Expand Down Expand Up @@ -95,6 +183,7 @@ export default function ExplorerListItem({ fileId, icon }) {
fileId={fileId}
type={file.objectType}
icon={icon}
uploadInfo={uploadInfo}
onDoubleClick={handleOpenFile}
onContextMenu={handleRightClick}
/>
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/components/panels/xdc/CollectionWizard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,6 @@ export default function CollectionWizard() {
selectedRepo,
status: response?.status || 'success',
}

setUploads((currentUploads) => [...(currentUploads || []), uploadEntry])
} catch (error) {
showErrorNotification('Upload failed', error?.response?.data?.error || error.message || 'Unable to upload the collection metadata.')
Expand All @@ -266,7 +265,7 @@ export default function CollectionWizard() {
return (
<Container style={stepperContainerStyle}>
<Stack gap="xl">
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<div>
<Dropzone
allowedTypes={[ObjectTypes.Metadata.id]}
item={metadataFile?.name}
Expand Down
Loading
Loading