diff --git a/package.json b/package.json
index 27465f31c..951a0d77d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "harperdb-studio",
- "version": "4.3.2",
+ "version": "4.3.3",
"description": "A UI for HarperDB",
"deploymentUrl": "studio.harperdb.io",
"private": true,
diff --git a/src/assets/styles/components/_web-ide.scss b/src/assets/styles/components/_web-ide.scss
index 5fd2b2148..f4473d66b 100644
--- a/src/assets/styles/components/_web-ide.scss
+++ b/src/assets/styles/components/_web-ide.scss
@@ -5,7 +5,7 @@
}
}
- color: white;
+ color: $color-white;
.editor.current-file-path {
border-bottom: 1px solid transparent;
@@ -13,7 +13,7 @@
.default-window-option {
span {
- color: white;
+ color: $color-white;
}
}
@@ -50,7 +50,7 @@
}
.file-browser button > span {
- color: white !important;
+ color: $color-white !important;
}
.file-browser .file.file-selected > span {
@@ -74,23 +74,25 @@
.default-window-option span {
- color: white !important;
+ color: $color-white !important;
}
.editor-window .default-window .default-window-container {
background: $color-pureblack !important;
- color: white;
+ color: $color-white;
}
.no-projects {
- color: white;
+ color: $color-white;
.docs-link {
color: #ee81ee !important;
}
}
- color: white;
+ color: $color-white;
+
+ color: rgb(105, 105, 105);
color: rgb(105, 105, 105);
@@ -128,11 +130,11 @@
}
.editor-window .cancel-button:hover {
- color: white;
+ color: $color-white;
}
.package-install-window {
- background: white;
+ background: $color-white;
}
.editor.current-file-path {
@@ -162,14 +164,6 @@
background: rgb(26, 26, 26);
}
- /*
- .root {
- display: grid;
- grid-template-columns: 150px minmax(0px, auto);
- height: 100%;
- }
- */
- // background: transparent !important;
}
.file-menu,
@@ -177,6 +171,7 @@
border-bottom: 1px solid darkgray !important;
}
+ .revert-file[disabled],
.save-code[disabled] {
color: #c2c2c2 !important;
}
@@ -228,7 +223,7 @@
color: rgb(105, 105, 105);
.package-install-window {
- background: white;
+ background: $color-white;
}
.editor.current-file-path {
@@ -265,7 +260,7 @@
}
.editor-window button:hover {
- color: white;
+ color: $color-white;
//rgb(105,105,105);
}
@@ -293,6 +288,22 @@
}
.web-ide {
+ .button-with-loader {
+ min-width: 105px;
+
+ &.loading {
+ color: transparent;
+ }
+
+ .button-with-loader-icon {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ }
+
+ }
.cancel-button {
margin-left: 10px;
}
@@ -352,7 +363,7 @@
.add-file-icon {
background: transparent;
border: none;
- color: white;
+ color: $color-white;
margin: 0;
outline: none !important;
padding: 0;
@@ -371,7 +382,7 @@
padding: 20px;
.file-menu {
- color: white;
+ color: $color-white;
display: flex;
flex-direction: row;
@@ -502,7 +513,7 @@
.editor-menu {
align-items: baseline;
border-bottom: 1px solid #2e2e2e;
- color: white;
+ color: $color-white;
display: flex;
flex-direction: row;
justify-content: left;
@@ -536,6 +547,7 @@
}
}
+ .revert-file,
.save-code {
background: transparent;
border: none;
@@ -546,6 +558,7 @@
padding: 0;
}
+ .revert-file[disabled],
.save-code[disabled] {
color: #5e5e5e;
cursor: default;
@@ -828,11 +841,11 @@
width: 40px;
i {
- color: white;
+ color: $color-white;
&.not-searching {
- color: white; //rgba(0,255,0, 0.1);
+ color: $color-white; //rgba(0,255,0, 0.1);
}
diff --git a/src/components/instance/browse/BrowseDatatable.js b/src/components/instance/browse/BrowseDatatable.js
index 58e699f06..fd4e70716 100644
--- a/src/components/instance/browse/BrowseDatatable.js
+++ b/src/components/instance/browse/BrowseDatatable.js
@@ -115,42 +115,50 @@ function BrowseDatatable({ tableState, setTableState, activeTable }) {
return (
<>
- setLastUpdate(Date.now())}
- toggleAutoRefresh={() => setTableState({ ...tableState, autoRefresh: !tableState.autoRefresh })}
- toggleFilter={() => setTableState({ ...tableState, showFilter: !tableState.showFilter })}
- />
-
-
- {
- setTableState({ ...tableState, page: 0, filtered: value })
- }}
- onSortedChange={(value) => setTableState({ ...tableState, page: 0, sorted: value })}
- onPageChange={(value) => setTableState({ ...tableState, page: value })}
- onPageSizeChange={(value) => setTableState({ ...tableState, page: 0, pageSize: value })}
- onRowClick={(rowData) => {
- const hashValue = rowData[tableState.hashAttribute];
- navigate(`/o/${customer_id}/i/${compute_stack_id}/browse/${schema}/${table}/edit/${rowData[tableState.hashAttribute]}`, {
- state: { hashValue }
- })
- }}
- />
-
-
+ setLastUpdate(Date.now())}
+ toggleAutoRefresh={() => setTableState({ ...tableState, autoRefresh: !tableState.autoRefresh })}
+ toggleFilter={() => setTableState({ ...tableState, showFilter: !tableState.showFilter })}
+ />
+
+
+ {
+ setTableState({ ...tableState, page: 0, filtered: value })
+ }}
+ onSortedChange={(value) => setTableState({ ...tableState, page: 0, sorted: value })}
+ onPageChange={(value) => setTableState({ ...tableState, page: value })}
+ onPageSizeChange={(value) => setTableState({ ...tableState, page: 0, pageSize: value })}
+ onRowClick={(rowData) => {
+
+ // encode schema, table and hashValue because they can contain uri components
+ const hashValue = rowData[tableState.hashAttribute];
+ const encodedSchema = encodeURIComponent(schema);
+ const encodedTable = encodeURIComponent(table);
+ const encodedHash = encodeURIComponent(hashValue);
+
+ const recordViewUrl = `/o/${customer_id}/i/${compute_stack_id}/browse/${encodedSchema}/${encodedTable}/edit/${encodedHash}`;
+ const navigateOptions = { state: { hashValue } };
+
+ navigate(recordViewUrl, navigateOptions);
+
+ }}
+ />
+
+
>
);
}
diff --git a/src/components/instance/browse/JSONEditor.js b/src/components/instance/browse/JSONEditor.js
index 8774d7d24..1b5249180 100644
--- a/src/components/instance/browse/JSONEditor.js
+++ b/src/components/instance/browse/JSONEditor.js
@@ -15,7 +15,21 @@ import addError from '../../../functions/api/lms/addError';
import ErrorFallback from '../../shared/ErrorFallback';
function JSONEditor({ newEntityAttributes, hashAttribute }) {
- const { customer_id, schema, table, hash, action, compute_stack_id } = useParams();
+
+ const {
+ customer_id,
+ schema: encodedSchema,
+ table: encodedTable,
+ hash: encodedHash,
+ action,
+ compute_stack_id
+ } = useParams();
+
+ // hash, table and schema can have uri components
+ const schema = decodeURIComponent(encodedSchema);
+ const table = decodeURIComponent(encodedTable);
+ const hash = decodeURIComponent(encodedHash);
+
const alert = useAlert();
const { state: locationState } = useLocation();
const navigate = useNavigate();
diff --git a/src/components/instance/functions/manage/index.js b/src/components/instance/functions/manage/index.js
index 00d394856..f36c4b0d8 100644
--- a/src/components/instance/functions/manage/index.js
+++ b/src/components/instance/functions/manage/index.js
@@ -14,6 +14,7 @@ import deployComponent from '../../../../functions/api/instance/deployComponent'
import restartService from '../../../../functions/api/instance/restartService';
import useInstanceAuth from '../../../../functions/state/instanceAuths';
+import useEditorCache from '../../../../functions/state/editorCache';
import ApplicationsEditor from '../../../shared/webide';
import CustomFunctionsEditor from './CustomFunctionsEditor';
@@ -45,19 +46,19 @@ function getDeployTargets(instanceList, instanceAuthList, thisCsId, auth) {
return memo;
}
- const [ major, minor ] = deployTarget?.version.split('.') || [];
+ const [ major, minor ] = deployTarget?.version.split('.') || [];
// exclude < 4.2
if (parseInt(major, 10) >= 4 && parseInt(minor, 10) >= 2) {
- memo.push({
+ memo.push({
isCurrentInstance: csId === thisCsId,
auth,
instance
});
- }
+ }
return memo;
@@ -69,17 +70,44 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
const { compute_stack_id } = useParams();
const registration = useStoreState(instanceState, (s) => s.registration);
- const { fileTree } = useStoreState(instanceState, (s) => s.custom_functions);
+ const { fileTree } = useStoreState(instanceState, (s) => s.custom_functions);
const auth = useStoreState(instanceState, (s) => s.auth);
const url = useStoreState(instanceState, (s) => s.url);
const [majorVersion, minorVersion] = (registration?.version || '').split('.') || [];
const supportsApplicationsAPI = parseFloat(`${majorVersion}.${minorVersion}`) >= 4.2;
const instances = useStoreState(appState, (s) => s.instances);
- const [instanceAuths] = useInstanceAuth({});
+ const [ instanceAuths ] = useInstanceAuth({});
+ const [ editorCache, setEditorCache ] = useEditorCache({});
const theme = useStoreState(appState, (s) => s.theme);
const [ restartingInstance, setRestartingInstance ] = useState(false);
const alert = useAlert();
+ function removeFileFromLocalStorage({ path }) {
+
+ const updatedCache = {...editorCache};
+ const fileKey = `${compute_stack_id}_${path}`;
+
+ if (fileKey in updatedCache) {
+ delete updatedCache[fileKey];
+ }
+
+ setEditorCache({
+ ...updatedCache
+ });
+ }
+
+ async function saveFileToLocalStorage(selectedFile) {
+ const { path, content } = selectedFile;
+
+ const fileKey = `${compute_stack_id}_${path}`;
+
+ setEditorCache({
+ ...editorCache,
+ [fileKey]: content
+ });
+
+ }
+
async function restartWithLoadingState({ auth: instanceAuth, url: instanceUrl }) {
setRestartingInstance(true);
@@ -96,9 +124,14 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
}
// save file to instance
- async function saveCodeToInstance(selectedFile, restartRequired) {
-
- const filepathRelativeToProjectDir = selectedFile.path.split('/').slice(2).join('/');
+ async function saveFileToInstance(selectedFile, restartRequired) {
+
+ // handle cached situation
+ // - NOTE: this 'selectedFile' is not reactive.
+ // - remove cache entry for this file
+ // - update selectedFile with new content
+ // - set selectedFile.cached = false.
+ const filepathRelativeToProjectDir = selectedFile.path.split('/').slice(2).join('/');
const payload = {
auth,
url,
@@ -113,11 +146,13 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
alert.error(message);
}
-
if (restartRequired) {
await restartWithLoadingState({ auth, url });
}
+ removeFileFromLocalStorage({ path: selectedFile.path });
+ selectedFile.cached = false;
+
await refreshCustomFunctions();
}
@@ -156,7 +191,25 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
const { path, project, name } = selectedFile;
const newFile = getRelativeFilepath(path);
- const { error, message } = await getComponentFile({
+ const fileCacheKey = `${compute_stack_id}_${path}`;
+ const cachedFile = editorCache[fileCacheKey];
+ const isCached = fileCacheKey in editorCache;
+
+ if (isCached) {
+
+ return {
+ cached: isCached,
+ content: cachedFile,
+ path,
+ project,
+ name
+ };
+
+ }
+
+ // TODO: set file content to local storage copy if it exists.
+ //
+ const { error, message: content } = await getComponentFile({
auth,
url,
project,
@@ -165,19 +218,21 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
if (error) {
- alert.error(message);
+ alert.error(content);
return {
content: '',
path,
project,
- name
+ name,
+ cached: false
};
}
return {
- content: message,
+ cached: isCached,
+ content,
path,
project,
name
@@ -423,14 +478,42 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
}
+ async function revertFileChanges(selectedFile) {
+
+ // ditch local storage version
+ removeFileFromLocalStorage({ path: selectedFile.path });
+
+ // get file
+ //
+ const { error, message} = await getComponentFile({
+ auth,
+ url,
+ project: selectedFile.project,
+ file: getRelativeFilepath(selectedFile.path)
+ });
+
+ removeFileFromLocalStorage({ path: selectedFile.path });
+
+ if (error) {
+ return alert.error(message);
+ }
+
+ await refreshCustomFunctions();
+
+ return message;
+
+ }
+
return supportsApplicationsAPI ?
{
+
+ setLoading(true);
+
+ try {
+
+ await onClick();
+ setLoading(false);
+
+ } catch(e) {
+ setLoading(false);
+ }
+
+ }
+ }
+ className={
+ cn(`${className} button-with-loader`, { loading })
+ }>
+ {
+ loading ?
+
+ : children
+ }
+
+ );
+}
+
+
diff --git a/src/components/shared/webide/Editor.js b/src/components/shared/webide/Editor.js
index 0726ed944..8a673a2ab 100644
--- a/src/components/shared/webide/Editor.js
+++ b/src/components/shared/webide/Editor.js
@@ -19,7 +19,7 @@ const extensionToLanguageMap = {
// TODO: update code using whatever monaco hook is available. onupdate.
// don't allow save if there are errors.
-function Editor({ active, file, onChange, theme }) {
+function Editor({ active, file, onFileChange, theme }) {
const [ language, setLanguage ] = useState('javascript');
const [ editorTheme, setEditorTheme ] = useState('vs-dark');
@@ -44,15 +44,16 @@ function Editor({ active, file, onChange, theme }) {
// eslint-disable-next-line no-unused-vars
return <>
-
- {filepathRelativeToComponentsDir}
+ path: {filepathRelativeToComponentsDir}
+ size: {file.size} bytes
+ last saved: {file.mtime}
{
setLoading(true);
- await onSave();
+ await onClick();
setTimeout(() => {
setLoading(false);
}, 500);
@@ -50,10 +50,10 @@ export function RestartInstanceButton({ onClick, restarting }) {
);
}
-export function RestartOnSaveToggle({ restartAfterSave, onClick }) {
+export function RestartOnSaveToggle({ onClick, restartAfterSave }) {
const title = restartAfterSave ?
- 'your instance will restart after saving application files' :
+ 'restart instance after saving application files' :
'your instance will not restart after saving application files';
return (
@@ -74,7 +74,22 @@ export function RestartOnSaveToggle({ restartAfterSave, onClick }) {
);
}
-export default function EditorMenu({ SaveButton: SaveBtn, RestartInstanceButton: RestartInstanceBtn, RestartOnSaveToggle: RestartOnSaveTgl }) {
+export function RevertFileButton({ onClick, disabled }) {
+ return (
+
+
+ )
+}
+
+// TODO: convert to mapping children to a list instead of explicitly doing all of this
+export default function EditorMenu({ SaveButton: SaveBtn, RestartInstanceButton: RestartInstanceBtn, RestartOnSaveToggle: RestartOnSaveTgl, RevertFileButton: RevertFileBtn }) {
return (
-
@@ -86,6 +101,9 @@ export default function EditorMenu({ SaveButton: SaveBtn, RestartInstanceButton:
-
+ -
+
+
)
}
diff --git a/src/components/shared/webide/FileMenu.js b/src/components/shared/webide/FileMenu.js
index 17d75bae9..c84a03cf4 100644
--- a/src/components/shared/webide/FileMenu.js
+++ b/src/components/shared/webide/FileMenu.js
@@ -14,12 +14,12 @@ export function DeleteFolderButton({ onClick, disabled }) {
)
}
-export function AddProjectFolderButton({ onAddProjectFolder, disabled }) {
+export function AddProjectFolderButton({ onClick, disabled }) {
return (
)
@@ -42,12 +42,12 @@ export function InstallPackageButton({ onClick, text="" }) {
}
-export function AddProjectButton({ disabled=false, onAddProject, text="" }) {
+export function AddProjectButton({ onClick, disabled=false, text="" }) {
return (
}
- Delete
+
+ Delete
+
Cancel
diff --git a/src/components/shared/webide/windows/DeletePackageWindow.js b/src/components/shared/webide/windows/DeletePackageWindow.js
index 6f88ade88..cdec8f499 100644
--- a/src/components/shared/webide/windows/DeletePackageWindow.js
+++ b/src/components/shared/webide/windows/DeletePackageWindow.js
@@ -1,5 +1,6 @@
import React from 'react';
import { Card, CardTitle, CardBody } from 'reactstrap';
+import ButtonWithLoader from '../../ButtonWithLoader';
export default function DeletePackageWindow({ active, selectedPackage, onConfirm, onCancel }) {
@@ -15,7 +16,11 @@ export default function DeletePackageWindow({ active, selectedPackage, onConfirm
Delete Confirmation
Are you sure you want to delete package { packageName } ?
-
Delete
+
+ Delete
+
Cancel
diff --git a/src/components/shared/webide/windows/NameInput.js b/src/components/shared/webide/windows/NameInput.js
index 413b86107..a053712d6 100644
--- a/src/components/shared/webide/windows/NameInput.js
+++ b/src/components/shared/webide/windows/NameInput.js
@@ -3,11 +3,10 @@
import React, { useState } from 'react';
import cn from 'classnames';
+import ButtonWithLoader from '../../ButtonWithLoader';
export default function NameInput({ onCancel, onConfirm, onEnter, label='', placeholder='', value, validate=() => true }) {
-
-
const [ name, setName ] = useState(value || '');
const [ isValidName, setIsValidName ] = useState(false);
@@ -36,7 +35,7 @@ export default function NameInput({ onCancel, onConfirm, onEnter, label='', plac
- { label &&
}
+ { label &&
}
);
diff --git a/src/functions/state/editorCache.js b/src/functions/state/editorCache.js
new file mode 100644
index 000000000..133d74be0
--- /dev/null
+++ b/src/functions/state/editorCache.js
@@ -0,0 +1,3 @@
+import createPersistedState from 'use-persisted-state';
+
+export default createPersistedState('editorCache');