Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upload and download data from and to volumes #95

Merged
merged 14 commits into from
Nov 20, 2024
2 changes: 1 addition & 1 deletion albatross_json.ml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ let success = function
| `Block_devices bs -> block_infos bs
| `Old_unikernels _ -> `String "old unikernels not supported"
| `Unikernel_image _ -> `String "unikernel image not supported"
| `Block_device_image _ -> `String "block device image not supported"
| `Block_device_image (_, bd) -> `String bd

let console_data_to_json (ts, data) =
`Assoc
Expand Down
129 changes: 126 additions & 3 deletions assets/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ function postAlert(bg_color, content) {
alertContainer.classList.remove("block", `${bg_color}`)
alertContainer.classList.add("hidden")
alertContainer.removeChild(alert);
}, 2500);
}, 4000);
}

async function deployUnikernel() {
Expand Down Expand Up @@ -579,10 +579,32 @@ async function createVolume() {
const block_compressed = document.getElementById("block_compressed").checked;
const block_data = document.getElementById("block_data").files[0];

if (!isValidName(block_name)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for refining the error message. Now, I'd appreciate if the deployUnikernel() as well gets such nice informative error messages, and best have both share the code....

So, could "isValidName" return the error message?

if (!block_name || block_name === '') {
formAlert.classList.remove("hidden", "text-primary-500");
formAlert.classList.add("text-secondary-500");
formAlert.textContent = "Please enter a name for this volume"
formAlert.textContent = "Please enter a name"
buttonLoading(createButton, false, "Create volume")
return;
}
if (!isLengthValid(block_name)) {
formAlert.classList.remove("hidden", "text-primary-500");
formAlert.classList.add("text-secondary-500");
formAlert.textContent = "The name must have at least 1 character and must not exceed 63 characters."
buttonLoading(createButton, false, "Create volume")
return;
}
if (!isStartingCharacterValid(block_name)) {
formAlert.classList.remove("hidden", "text-primary-500");
formAlert.classList.add("text-secondary-500");
formAlert.textContent = "The name cannot start with a hyphen (-)."
buttonLoading(createButton, false, "Create volume")
return;
}
if (!areCharactersValid(block_name)) {
formAlert.classList.remove("hidden", "text-primary-500");
formAlert.classList.add("text-secondary-500");
formAlert.textContent = "Only letters (a-z, A-Z), digits (0-9), hyphens (-), and periods (.) are permitted.\
Special characters, spaces, and symbols other than the specified ones are not allowed"
buttonLoading(createButton, false, "Create volume")
return;
}
Expand Down Expand Up @@ -639,6 +661,89 @@ async function createVolume() {
}
}

async function downloadVolume(block_name) {
const downloadButton = document.getElementById(`download-block-button-${block_name}`);
const compression_level = document.getElementById("compression-level").innerText;
const molly_csrf = document.getElementById("molly-csrf").value;
try {
buttonLoading(downloadButton, true, "Downloading...")
const response = await fetch("/api/volume/download", {
method: 'POST',
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(
{
"block_name": block_name,
"compression_level": Number(compression_level),
"molly_csrf": molly_csrf
})
})
if (!response.ok) {
const data = await response.json();
postAlert("bg-secondary-300", data.data);
buttonLoading(downloadButton, false, `Download ${block_name}`)
} else {
// trigger a download
const filename = response.headers.get('content-disposition')
.split(';')[1].split('=')[1].replace(/"/g, '');
const blob = await response.blob();
const downloadLink = document.createElement('a');
downloadLink.href = URL.createObjectURL(blob);
downloadLink.download = filename;
downloadLink.click();
URL.revokeObjectURL(downloadLink.href);
postAlert("bg-primary-300", "Download in progress");
buttonLoading(downloadButton, false, `Download ${block_name}`)
}
} catch (error) {
postAlert("bg-secondary-300", error);
buttonLoading(downloadButton, false, `Download ${block_name}`)
}
}

async function uploadToVolume(block_name) {
const uploadButton = document.getElementById(`upload-block-button-${block_name}`);
const block_compressed = document.getElementById("block_compressed").checked;
const block_data = document.getElementById("block_data").files[0];
const molly_csrf = document.getElementById("molly-csrf").value;

if (!block_data) {
postAlert("bg-secondary-300", "Please select a file to be uploaded");
buttonLoading(uploadButton, false, "Upload data")
return;
}

try {
buttonLoading(uploadButton, true, "Uploading...")
let formData = new FormData();
let json_data = JSON.stringify(
{
"block_name": block_name,
"block_compressed": block_compressed,
"molly_csrf": molly_csrf
})
formData.append("block_data", block_data)
formData.append("json_data", json_data)
const response = await fetch("/api/volume/upload", {
method: 'POST',
body: formData
})
const data = await response.json();
if (data.status === 200) {
postAlert("bg-primary-300", `Upload is succesful: ${data.data}`);
setTimeout(() => window.location.reload(), 1000);
buttonLoading(uploadButton, false, "Upload data")
} else {
postAlert("bg-secondary-300", data.data);
buttonLoading(uploadButton, false, "Upload data")
}
} catch (error) {
postAlert("bg-secondary-300", error);
buttonLoading(uploadButton, false, "Upload data")
}
}

function isValidName(s) {
const length = s.length;
if (length === 0 || length >= 64) return false;
Expand All @@ -652,3 +757,21 @@ function isValidName(s) {
return true;
}

function isLengthValid(s) {
const length = s.length;
return length > 0 && length < 64;
}

function isStartingCharacterValid(s) {
return s[0] !== '-';
}

function areCharactersValid(s) {
for (let i = 0; i < s.length; i++) {
const char = s[i];
if (!(/[a-zA-Z0-9.-]/).test(char)) {
return false;
}
}
return true;
}
2 changes: 1 addition & 1 deletion assets/style.css

Large diffs are not rendered by default.

20 changes: 6 additions & 14 deletions dashboard.ml
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ let dashboard_layout ~csrf (user : User_model.user) ~icon
a_id "alert-container";
a_class
[
"absolute top-1/4 rounded-md right-4 z-50 w-fit \
"fixed top-1/4 rounded-md right-4 z-50 w-fit \
space-y-2 p-4 shadow text-wrap hidden";
];
]
Expand Down Expand Up @@ -545,19 +545,11 @@ let dashboard_layout ~csrf (user : User_model.user) ~icon
];
]
else div []);
button
~a:
[
a_id "logout-button";
a_onclick "logout()";
a_class
[
"my-3 py-3 rounded bg-secondary-500 \
hover:bg-secondary-800 w-full text-gray-50 \
font-semibold";
];
]
[ txt "Logout" ];
Utils.button_component
~attribs:
[ a_id "logout-button"; a_onclick "logout()" ]
~content:(txt "Logout") ~extra_css:"w-full my-2"
~btn_type:`Danger_full ();
];
];
section
Expand Down
73 changes: 73 additions & 0 deletions modal_dialog.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
let modal_dialog ~modal_title ~button_content ?(button_type = `Primary_full)
~content () =
Tyxml_html.(
section ~a:[]
[
div
~a:[ Unsafe.string_attrib "x-data" "{modalIsOpen: false}" ]
[
Utils.button_component ~content:button_content ~btn_type:button_type
~attribs:
[ Unsafe.string_attrib "x-on:click" "modalIsOpen = true" ]
();
div
~a:
[
Unsafe.string_attrib "x-cloak" "";
Unsafe.string_attrib "x-show" "modalIsOpen";
Unsafe.string_attrib "x-transition.opacity.duration.200ms" "";
Unsafe.string_attrib "x-trap.inert.noscroll" "modalIsOpen";
Unsafe.string_attrib "x-on:keydown.esc.window"
"modalIsOpen = false";
Unsafe.string_attrib "x-on:click.self" "modalIsOpen = false";
a_class
[
"fixed inset-0 z-30 flex items-end justify-center \
bg-black/20 p-4 backdrop-blur-md sm:items-center";
];
a_role [ "dialog" ];
a_aria "modal" [ "true" ];
]
[
div
~a:
[
Unsafe.string_attrib "x-show" "modalIsOpen";
Unsafe.string_attrib "x-transition:enter"
"transition ease-out duration-200 delay-100 \
motion-reduce:transition-opacity";
Unsafe.string_attrib "x-transition:enter-start"
"opacity-0 scale-50";
Unsafe.string_attrib "x-transition:enter-end"
"opacity-100 scale-100";
a_class
[
"flex max-w-xl flex-col gap-4 overflow-hidden \
rounded-md border border-neutral-300 bg-gray-50";
];
]
[
div
~a:
[
a_class
[ "flex items-center justify-between border-b p-4" ];
]
[
h3
~a:[ a_class [ "font-bold text-gray-700" ] ]
[ txt modal_title ];
i
~a:
[
a_class [ "fa-solid fa-x text-sm cursor-pointer" ];
Unsafe.string_attrib "x-on:click"
"modalIsOpen = false";
]
[];
];
div ~a:[ a_class [ "px-4" ] ] [ content ];
];
];
];
])
62 changes: 15 additions & 47 deletions settings_page.ml
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,10 @@ let settings_layout (configuration : Configuration.t) =
div
~a:[ a_id "add-config" ]
[
button
~a:
[
a_onclick "openConfigForm('','','','')";
a_class
[
"inline-flex items-center gap-x-2 text-sm \
font-semibold rounded-lg border border-1 py-1 px-2 \
border-primary-400 text-primary-600 \
hover:text-primary-500 focus:outline-none \
focus:text-primary-800 disabled:opacity-50 \
disabled:pointer-events-none";
];
]
[ i ~a:[ a_class [ "fa-solid fa-plus" ] ] [] ];
Utils.button_component
~attribs:[ a_onclick "openConfigForm('','','','')" ]
~content:(i ~a:[ a_class [ "fa-solid fa-plus" ] ] [])
~btn_type:`Primary_outlined ();
];
];
hr ~a:[ a_class [ "border border-primary-500 my-5" ] ] ();
Expand Down Expand Up @@ -199,18 +188,10 @@ let settings_layout (configuration : Configuration.t) =
div
~a:[ a_class [ "mx-auto my-4 flex justify-center" ] ]
[
button
~a:
[
a_onclick "saveConfig()";
a_id "config-button";
a_class
[
"py-3 px-3 rounded bg-primary-500 \
hover:bg-primary-800 text-gray-50 font-semibold";
];
]
[ txt "" ];
Utils.button_component
~attribs:
[ a_onclick "saveConfig()"; a_id "config-button" ]
~content:(txt "") ~btn_type:`Primary_full ();
];
];
];
Expand Down Expand Up @@ -373,33 +354,20 @@ let settings_layout (configuration : Configuration.t) =
];
]
[
button
~a:
Utils.button_component
~attribs:
[
a_onclick
("openConfigForm('" ^ ip ^ "','"
^ port ^ "','"
^ String.escaped certificate ^ "','"
^ String.escaped private_key ^ "')");
a_class
[
"inline-flex items-center \
gap-x-2 text-sm font-semibold \
rounded-lg border border-1 py-1 \
px-2 border-primary-400 \
text-primary-600 \
hover:text-primary-500 \
focus:outline-none \
focus:text-primary-800 \
disabled:opacity-50 \
disabled:pointer-events-none";
];
]
[
i
~a:[ a_class [ "fa-solid fa-pen" ] ]
[];
];
~content:
(i
~a:[ a_class [ "fa-solid fa-pen" ] ]
[])
~btn_type:`Primary_outlined ();
];
];
];
Expand Down
18 changes: 5 additions & 13 deletions sign_in.ml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ let login_page ~icon () =
a_id "alert-container";
a_class
[
"absolute top-1/4 rounded-md right-4 z-50 w-fit \
"fixed top-1/4 rounded-md right-4 z-50 w-fit \
space-y-2 p-4 shadow text-wrap hidden";
];
]
Expand Down Expand Up @@ -172,18 +172,10 @@ let login_page ~icon () =
];
div
[
button
~a:
[
a_id "login-button";
a_class
[
"py-3 rounded bg-primary-500 \
hover:bg-primary-800 w-full \
text-gray-50 font-semibold";
];
]
[ txt "Sign In" ];
Utils.button_component
~attribs:[ a_id "login-button" ]
~content:(txt "Sign In")
~btn_type:`Primary_full ();
];
];
];
Expand Down
Loading
Loading