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 ( + - + + Delete + + diff --git a/src/components/shared/webide/windows/DeleteFolderWindow.js b/src/components/shared/webide/windows/DeleteFolderWindow.js index 12112bb1c..5a75546bb 100644 --- a/src/components/shared/webide/windows/DeleteFolderWindow.js +++ b/src/components/shared/webide/windows/DeleteFolderWindow.js @@ -1,5 +1,6 @@ import React from 'react'; import { Card, CardTitle, CardBody } from 'reactstrap'; +import ButtonWithLoader from '../../ButtonWithLoader'; export default function DeleteFolderWindow({ active, selectedFolder, onConfirm, onCancel }) { @@ -24,7 +25,11 @@ export default function DeleteFolderWindow({ active, selectedFolder, onConfirm, {projectSubdir} from project { project } ?

} - + + Delete + 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 +
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 && }
0 && !isValidName }) } @@ -61,15 +60,17 @@ export default function NameInput({ onCancel, onConfirm, onEnter, label='', plac
- - + onClick={ () => onConfirm(name) } + >OK +
); 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');