import React, { useState, useEffect, useContext, memo } from "react"; import { toast } from 'react-toastify'; import { GetIconInfo, } from "../views/Workflows2.jsx"; import { IconButton, List, ListItem, ListItemText, ListItemAvatar, ListItemSecondaryAction, Tooltip, Button, ButtonGroup, FormControl, InputLabel, TextField, Divider, Select, MenuItem, Dialog, DialogTitle, DialogContent, DialogActions, Typography, Skeleton, Checkbox, Chip, Menu, Pagination, PaginationItem, } from "@mui/material"; import { DataGrid } from "@mui/x-data-grid"; import { Link as LinkIcon, OpenInNew as OpenInNewIcon, Edit as EditIcon, CloudDownload as CloudDownloadIcon, Delete as DeleteIcon, FileCopy as FileCopyIcon, Cached as CachedIcon, Publish as PublishIcon, Clear as ClearIcon, Add as AddIcon, SelectAll, } from "@mui/icons-material"; import Dropzone from "../components/Dropzone.jsx"; import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx"; import {getTheme} from "../theme.jsx"; import { Context } from "../context/ContextApi.jsx"; import { red } from "../views/AngularWorkflow.jsx"; const Files = memo((props) => { const { globalUrl, userdata, serverside, selectedOrganization, isCloud,isSelectedFiles } = props; const [files, setFiles] = React.useState([]); const [showLoader, setShowLoader] = useState(true) const [selectedCategory, setSelectedCategory] = React.useState("default"); const [openFileId, setOpenFileId] = React.useState(false); const [fileCategories, setFileCategories] = React.useState([]); const [fileContent, setFileContent] = React.useState(""); const [openEditor, setOpenEditor] = React.useState(false); const [renderTextBox, setRenderTextBox] = React.useState(false); const [loadFileModalOpen, setLoadFileModalOpen] = React.useState(false); const { themeMode, brandColor } = useContext(Context); const theme = getTheme(themeMode, brandColor); const [field1, setField1] = React.useState(""); const [field2, setField2] = React.useState(""); const [downloadUrl, setDownloadUrl] = React.useState("https://github.com/shuffle/standards") const [downloadBranch, setDownloadBranch] = React.useState("main"); const [downloadFolder, setDownloadFolder] = React.useState("translation_standards"); const [contentLoading, setContentLoading] = React.useState(false) const [selectAllChecked, setSelectAllChecked] = React.useState(false) const [selectedFiles, setSelectedFiles] = useState([]); const [selectedFileId, setSelectedFileId] = useState([]) const [showFileCategoryPopup, setShowFileCategoryPopup] = useState(false) const [updateToThisCategory, setUpdateToThisCategory] = useState("") const [showDistributionPopup, setShowDistributionPopup] = useState(false) const [selectedSubOrg, setSelectedSubOrg] = useState([]) const [fileIdSelectedForDistribution, setFileIdSelectedForDistribution] = useState("") const [totalAmount, setTotalAmount] = useState(0); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(50) const [selectedRows, setSelectedRows] = useState([]); const [filesLoaded, setFilesLoaded] = useState(false); //const alert = useAlert(); const allowedFileTypes = ["txt", "py", "yaml", "yml","json", "html", "js", "csv", "log", "eml", "msg", "md", "xml", "sh", "bat", "ps1", "psm1", "psd1", "ps1xml", "pssc", "psc1", "response"] var upload = ""; const paginatedRows = files.slice(page * pageSize, (page + 1) * pageSize); const columns = [ { field : 'filename', headerName: 'Name', filterable: true, sortable: true, width: 250, renderCell: (params) => { if (params.row.filename === undefined || params.row.filename === null || params.row.filename.length < 1) { return ( No name ) } return ( {params?.row?.filename}
{params?.row?.tags} } placement="left" arrow > {params.row.filename}
); } }, { field: 'Workflow', headerName: 'Workflow', renderCell: (params) => { const file = params.row; return ( file.workflow_id === "global" || !file.workflow_id ? ( ) : ( ) ); }, }, { field: 'md5_sum', headerName: 'MD5', width: 100, }, { field: "Status", headerName: "Status", renderCell: (params) => { const file = params.row; return ( {file.status.charAt(0).toUpperCase() + file.status.slice(1)} ); } }, { field: "filesize", headerName: "Filesize", }, { field: "actions", headerName: "Actions", width: 200, renderCell: (params) => { const file = params.row; const filenamesplit = file.filename.split(".") const iseditable = file.filesize < 2000000 && file.status === "active" && (allowedFileTypes.includes(filenamesplit[filenamesplit.length-1]) || !file?.filename.includes(".")) return ( { e.stopPropagation(); e.preventDefault(); setOpenEditor(true) setOpenFileId(file.id) readFileData(file) }} > {/* { // Open the file, without downloading it window.open(`${globalUrl}/api/v1/files/${file.id}/content?type=text&authorization=${file.public_authorization}`, "_blank noreferrer noopener") }} > */} { e.stopPropagation(); e.preventDefault(); downloadFile(file); }} > { e.stopPropagation(); e.preventDefault(); navigator.clipboard.writeText(file.id); document.execCommand("copy"); toast(file.id + " copied to clipboard"); }} > { e.stopPropagation(); e.preventDefault(); deleteFile(file.id, true); }} > ) } }, { field: "distribution", headerName: "Distribution", renderCell: (params) => { const file = params.row; const isDistributed = file?.suborg_distribution?.length > 0 ? true : false; return ( <> {selectedOrganization.id !== undefined && file?.org_id !== selectedOrganization.id ? : { e.stopPropagation(); e.preventDefault(); setShowDistributionPopup(true) if(file?.suborg_distribution?.length > 0){ setSelectedSubOrg(file.suborg_distribution) }else{ setSelectedSubOrg([]) } setFileIdSelectedForDistribution(file.id) }} /> } ) } } ] const handleKeyDown = (event) => { if (event.key === 'Enter') { fileCategories.push(event.target.value); setSelectedCategory(event.target.value); setRenderTextBox(false); } if (event.key === 'Escape'){ // not working for some reasons console.log('escape pressed') setRenderTextBox(false); } } const changeDistribution = (id, selectedSubOrg) => { editFileConfig(id, "suborg_distribute", [...new Set(selectedSubOrg)]) } const editFileConfig = (id, parentAction, selectedSubOrg) => { const data = { id: id, action: parentAction !== undefined && parentAction !== null ? parentAction : "change_category", selected_suborgs: selectedSubOrg, } const url = globalUrl + "/api/v1/files/" + id + "/config"; fetch(url, { mode: "cors", method: "POST", body: JSON.stringify(data), credentials: "include", crossDomain: true, withCredentials: true, headers: { "Content-Type": "application/json; charset=utf-8", }, }) .then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { toast.error("Failed overwriting files"); } else { toast.success("File updated!"); setTimeout(() => { getFiles(); }, 1000); } }) ) .catch((error) => { toast("Err: " + error.toString()); }); }; const handleFileCheckboxChange = (index) => { setSelectedFiles((prevSelected) => { const updatedSelected = [...prevSelected]; updatedSelected[index] = !updatedSelected[index]; return updatedSelected; }); }; const runUpdateText = (text) =>{ fetch(`${globalUrl}/api/v1/files/${openFileId}/edit`, { method: "PUT", headers: { "Content-Type": "application/json", Accept: "application/json", }, body:text, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Can't update file"); } return response.json(); }) .then((responseJson) => { if (responseJson.success === true) { toast("Successfully updated file"); } }) .catch((error) => { toast("Error updating file: " + error.toString()); }) } const getFiles = (namespace) => { setFilesLoaded(false) var parsedurl = `${globalUrl}/api/v1/files` if (namespace === undefined || namespace === null || namespace === "default") { } else if (namespace !== undefined && namespace !== null && namespace !== "") { parsedurl = `${globalUrl}/api/v1/files/namespaces/${namespace}?ids=true` } else if (selectedCategory !== undefined && selectedCategory !== null && selectedCategory !== "default" && selectedCategory !== "") { parsedurl = `${globalUrl}/api/v1/files/namespaces/${selectedCategory}?ids=true` } fetch(parsedurl, { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for apps :O!"); return; } return response.json(); }) .then((responseJson) => { if (responseJson.files !== undefined && responseJson.files !== null) { setFiles(responseJson.files); if (responseJson.total_amount !== undefined && responseJson.total_amount !== null && responseJson.total_amount > 0) { setTotalAmount(responseJson.total_amount) } else { setTotalAmount(responseJson.files.length) } setShowLoader(false) setShowDistributionPopup(false) } else if (responseJson.list !== undefined && responseJson.list !== null) { // Set the "namespace" field in all items if (namespace !== undefined && namespace !== null) { responseJson.list.forEach((item) => { item.namespace = namespace item.filename = item.name item.workflow_id = "global" }) } setFiles(responseJson.list); setShowLoader(false) } else { setFiles([]); setShowLoader(false) setShowDistributionPopup(false) } if (namespace === undefined || namespace === null || namespace === "default") { if (responseJson.namespaces !== undefined && responseJson.namespaces !== null && (fileCategories.length === 0 || responseJson.namespaces.length > fileCategories.length)) { setFileCategories(responseJson.namespaces) } } }) .catch((error) => { toast(error.toString()); }).finally(() => { setFilesLoaded(true) }); }; useEffect(() => { getFiles("default") setTimeout(() => { var category = selectedCategory if (window.location.search.includes("category=")) { const urlParams = new URLSearchParams(window.location.search) category = urlParams.get("category") } if (category !== undefined && category !== null && category.length > 0 && category !== "default") { setSelectedCategory(category) } getFiles(category) }, 1000) }, []); const importStandardsFromUrl = (url, folder) => { if (url === undefined || url === null || url.length < 5) { toast("Please enter a valid URL"); return; } if (folder === undefined || folder === null || folder.length < 1) { toast("Please enter a valid folder name. For Root: /") return } const parsedData = { url: url, path: folder, field_3: downloadBranch || "master", namespace: selectedCategory !== undefined && selectedCategory !== null && selectedCategory !== "default" ? selectedCategory : "", }; if (field1.length > 0) { parsedData["field_1"] = field1; } if (field2.length > 0) { parsedData["field_2"] = field2; } toast(`Getting files from url ${url}. This may take a while if the repository is large. Please wait...`); fetch(globalUrl + "/api/v1/files/download_remote", { method: "POST", mode: "cors", headers: { Accept: "application/json", }, body: JSON.stringify(parsedData), credentials: "include", }) .then((response) => { if (response.status === 200) { toast("Successfully loaded files from " + downloadUrl); setLoadFileModalOpen(false); } return response.json(); }) .then((responseJson) => { if (!responseJson.success) { if (responseJson.reason !== undefined) { toast("Failed loading: " + responseJson.reason); } else { toast("Failed loading"); } } }) .catch((error) => { toast(error.toString()); }); } const handleGithubValidation = () => { importStandardsFromUrl(downloadUrl, downloadFolder); } const fileDownloadModal = loadFileModalOpen ?
Load Files from Github
Files will be loaded from the repository and branch you specify, with the focus on files in one folder at a time. This is NOT recursive.
Repository URL (supported: github, gitlab, bitbucket) setDownloadUrl(e.target.value)} placeholder="https://github.com/shuffle/standards" fullWidth />
Branch (default value is "main"): setDownloadBranch(e.target.value)} placeholder="master" fullWidth /> Folder (can use / for subfolders): setDownloadFolder(e.target.value)} placeholder="translation_standards" fullWidth />
Authentication (optional - private repos etc):
setField1(e.target.value)} type="username" placeholder="Username / APIkey (optional)" fullWidth /> setField2(e.target.value)} type="password" placeholder="Password (optional)" fullWidth />
: null const handleSelectSubOrg = (id, action) => { if (action === "all") { const childOrgs = userdata.orgs.filter( (data) => data.creator_org === userdata.active_org.id ); setSelectedSubOrg((prev) => { if (prev.length === childOrgs.length) { // If all child orgs are already selected, clear the selection return []; } else { // Otherwise, select all child org IDs return childOrgs.map((data) => data.id); } }); } else if (action === "none") { setSelectedSubOrg([]); } else { setSelectedSubOrg((prev) => { if (prev.includes(id)) { return prev.filter((data) => data !== id); } else { return [...prev, id]; } }); } }; const fileDistributionModal = showDistributionPopup ? ( setShowDistributionPopup(false)} PaperProps={{ sx: { borderRadius: theme?.palette?.DialogStyle?.borderRadius, border: theme?.palette?.DialogStyle?.border, fontFamily: theme?.typography?.fontFamily, backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, zIndex: 1000, minWidth: "600px", minHeight: "320px", overflow: "auto", '& .MuiDialogContent-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, '& .MuiDialogTitle-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, '& .MuiDialogActions-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, }, }} > Select sub-org to distribute files {handleSelectSubOrg(null, "none")}}>None {handleSelectSubOrg(null, "all")}}>All {userdata.orgs.map((data, index) => { if (data.creator_org !== userdata.active_org.id) { return null; } const imagesize = 22; const imageStyle = { width: imagesize, height: imagesize, pointerEvents: "none", marginRight: 10, marginLeft: data.id === userdata.active_org.id ? 0 : 20, }; const image = data.image === "" ? ( {data.name} ) : ( {data.name} ); return ( handleSelectSubOrg(data.id)} style={{ display: "flex", alignItems: "center" }} > {image} {data.name} ); })}
): null const deleteFile = (fileId, showSinglDeleteToast) => { console.log("Deleting file with ID: ", fileId) console.log("showSinglDeleteToast: ", showSinglDeleteToast) fetch(globalUrl + "/api/v1/files/" + fileId, { method: "DELETE", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for file delete :O!"); } return response.json(); }) .then((responseJson) => { if (responseJson.success && showSinglDeleteToast === true) { toast.success("Deleted file") } else if ( responseJson.reason !== undefined && responseJson.reason !== null ) { toast.error("Failed to delete file: " + responseJson.reason); } if (showSinglDeleteToast === true) { setTimeout(() => { getFiles(selectedCategory) }, 1500); } }) .catch((error) => { toast(error.toString()); }); }; const readFileData = (file) => { setContentLoading(true) fetch(globalUrl + "/api/v1/files/" + file.id + "/content", { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { setContentLoading(false) if (response.status !== 200) { console.log("Status not 200 for file :O!"); return ""; } return response.text(); }) .then((respdata) => { // console.log("respdata ->", respdata); // console.log("respdata type ->", typeof(respdata)); if (respdata.length === 0) { toast("Failed getting file. Is it deleted?"); return; } return respdata }) .then((responseData) => { setFileContent(responseData); //console.log("filecontent state ",fileContent); }) .catch((error) => { setContentLoading(false) toast(error.toString()) }); }; const downloadFile = (file) => { fetch(globalUrl + "/api/v1/files/" + file.id + "/content", { method: "GET", credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for apps :O!"); return ""; } console.log("Resp: ", response) return response.blob() }) .then((respdata) => { if (respdata.length === 0) { toast("Failed getting file. Is it deleted?"); return; } var blob = new Blob([respdata], { type: "application/octet-stream", }); var url = URL.createObjectURL(blob); var link = document.createElement("a"); link.setAttribute("href", url); link.setAttribute("download", `${file.filename}`); var event = document.createEvent("MouseEvents"); event.initMouseEvent( "click", true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null ); link.dispatchEvent(event); //return response.json() }) .then((responseJson) => { //console.log(responseJson) //setSchedules(responseJson) }) .catch((error) => { toast(error.toString()); }); }; const handleCreateFile = (filename, file) => { var data = { filename: filename, org_id: selectedOrganization.id, workflow_id: "global", }; if ( selectedCategory !== undefined && selectedCategory !== null && selectedCategory.length > 0 && selectedCategory !== "default" ) { data.namespace = selectedCategory; } fetch(globalUrl + "/api/v1/files/create", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", body: JSON.stringify(data), }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for apps :O!"); return; } return response.json(); }) .then((responseJson) => { //console.log("RESP: ", responseJson) if (responseJson.success === true) { handleFileUpload(responseJson.id, file); } else { toast("Failed to upload file ", filename); } }) .catch((error) => { toast("Failed to upload file ", filename); console.log(error.toString()); }); }; const handleFileUpload = (file_id, file) => { //console.log("FILE: ", file_id, file) fetch(`${globalUrl}/api/v1/files/${file_id}/upload`, { method: "POST", credentials: "include", body: file, }) .then((response) => { if (response.status !== 200 && response.status !== 201) { console.log("Status not 200 for apps :O!"); toast("File was created, but failed to upload."); return; } return response.json(); }) .then((responseJson) => { //console.log("RESPONSE: ", responseJson) //setFiles(responseJson) }) .catch((error) => { toast(error.toString()); }); }; const uploadFiles = (files) => { for (var key in files) { try { const filename = files[key].name; var filedata = new FormData(); filedata.append("shuffle_file", files[key]); if (typeof files[key] === "object") { handleCreateFile(filename, filedata); } /* reader.addEventListener('load', (e) => { var data = e.target.result; setIsDropzone(false) console.log(filename) console.log(data) console.log(files[key]) }) reader.readAsText(files[key]) */ } catch (e) { console.log("Error in dropzone: ", e); } } setTimeout(() => { getFiles() }, 3000); }; const uploadFile = (e) => { const isDropzone = e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0; const files = isDropzone ? e.dataTransfer.files : e.target.files; //const reader = new FileReader(); //toast("Starting fileupload") uploadFiles(files); }; const handleUpdateFileCategory = (namespace) => { if (selectedFiles.length === 0 && !selectAllChecked) { toast("Please select files to update category") return } if (namespace === undefined || namespace === null || namespace === "") { toast("Please select a category to update files to") return } const url = globalUrl + `/api/v1/files/namespaces/${namespace}/share` const data = { SelectedFiles: selectedFileId, } setShowLoader(true) fetch(url, { mode: "cors", method: "POST", body: JSON.stringify(data), credentials: "include", crossDomain: true, withCredentials: true, headers: { "Content-Type": "application/json; charset=utf-8", }, }) .then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { toast("Failed overwriting files"); } else { setSelectAllChecked(false) setSelectedFiles([]) setSelectedFileId([]) setShowFileCategoryPopup(false) setSelectedCategory(namespace) setTimeout(() => { getFiles(); toast("Successfully updated file!"); if (window.location.search.includes("category=")) { const newurl = window.location.href.replace(/category=[^&]+/, `category=${namespace}`) window.history.pushState({ path: newurl }, "", newurl) } else { window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${namespace}`) } }, 1000); } } )) .catch((error) => { toast("Err: " + error.toString()); }); } return ( {fileDistributionModal}
{fileDownloadModal}
Files Files from Workflows are a way to store as well as edit files.{" "} Learn more
{/* */} (upload = ref)} onChange={(event) => { //const file = event.target.value //const fileObject = URL.createObjectURL(actualFile) //setFile(fileObject) //const files = event.target.files[0] uploadFiles(event.target.files); }} /> {/*
*/} {selectedCategory === "sigma" || selectedCategory === "yara" ? : null} {fileCategories !== undefined && fileCategories !== null && fileCategories.length > 1 ? ( Category setShowFileCategoryPopup(false)}> File Categories Please note that your selected files ({selectedFileId?.length}) will be moved to the {updateToThisCategory} category. ) : null} {/*
*/} {renderTextBox ? : } {renderTextBox && { handleKeyDown(event); if(event.key === 'Enter' && selectedFileId.length > 0){ setShowFileCategoryPopup(true) setUpdateToThisCategory(event.target.value) } }} style={{ height: 35, width: 200, marginTop: 0, }} InputProps={{ style: { color: theme.palette.textFieldStyle.color, backgroundColor: theme.palette.textFieldStyle.backgroundColor, height: 35, fontSize: 16, borderRadius: 4, paddingTop: 0, }, }} color="primary" placeholder="File category name" required margin="dense" defaultValue={""} autoFocus />} {isSelectedFiles?null: }
{ setSelectedRows(newSelection); }} sx={{ marginTop: 1, height: files.length*52, width: "100%", '.MuiTablePagination-selectLabel, .MuiTablePagination-select, .MuiTablePagination-selectIcon': { display: 'none', }, marginBottom: 20, }} hideFooterSelectedRowCount={true} hideFooter={true} pagination autoHeight={true} getRowId={(row) => row.id} keepNonExistentRowsSelected={false} loading={filesLoaded === false} />
{page * pageSize + 1} - {Math.min((page + 1) * pageSize, totalAmount)} of {totalAmount} { return ( ) }} onChange={(e, value) => { if (value < 1) { return } const newPage = value-1 console.log("New page: ", value) // handleChangePage() setPage(newPage) }} /> {selectedRows.length > 0 ? : null}
) }) export default memo(Files); const DownloadFileIcon = memo(({ setLoadFileModalOpen, isSelectedFiles }) => { return ( setLoadFileModalOpen(true)} sx={{ position: "absolute", right: 0, top: isSelectedFiles ? null : 0, left: isSelectedFiles ? "93%" : null, transition: "left 0.3s ease", width: 48, height: 48, borderRadius: "50%", }} > ); });