diff --git a/.eslintrc b/.eslintrc
index 49a3684bb..201d888e9 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -14,6 +14,7 @@
"no-nested-ternary": 0,
"no-param-reassign": 0,
"no-return-assign": 0,
+ "no-restricted-syntax": 0,
"react/jsx-filename-extension": 0,
"react/jsx-props-no-spreading": 0,
"react/destructuring-assignment": 0,
@@ -21,6 +22,12 @@
"react/no-danger": 0,
"no-unused-vars": 1,
"react/button-has-type": 1,
- "no-underscore-dangle": ["warn", { "allow": ["_kmq", "_kmk"] }]
+ "no-underscore-dangle": ["warn", { "allow": ["_kmq", "_kmk"] }],
+ "jsx-a11y/label-has-associated-control": 0,
+ "react/jsx-no-bind": 0,
+ "react/no-unstable-nested-components": [
+ "warn",
+ {"allowAsProps": true }
+ ]
}
}
diff --git a/.github/workflows/publish-stage.yaml b/.github/workflows/publish-stage.yaml
index 7613ef371..43024b489 100644
--- a/.github/workflows/publish-stage.yaml
+++ b/.github/workflows/publish-stage.yaml
@@ -72,7 +72,7 @@ jobs:
#!/bin/bash
yarn
- yarn lint
+ yarn lint-prod
yarn build:stage
- name: Clean Remote Directory
env:
diff --git a/.gitignore b/.gitignore
index 095b8f392..934e75f19 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+.idea
/src/config/index.js
# dependencies
diff --git a/package.json b/package.json
index 14b154e69..65b190f97 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "harperdb-studio",
- "version": "4.2.11",
+ "version": "4.3.0",
"description": "A UI for HarperDB",
"deploymentUrl": "studio.harperdb.io",
"private": true,
@@ -11,7 +11,8 @@
"build:dev": "PUBLIC_URL=https://d2uv4fa0aeja0t.cloudfront.net react-scripts build",
"build:stage": "GENERATE_SOURCEMAP=false PUBLIC_URL=https://dbjxbnqel2bw9.cloudfront.net react-scripts build",
"build:prod": "GENERATE_SOURCEMAP=false PUBLIC_URL=https://ds5zz9rfvzuta.cloudfront.net react-scripts build",
- "lint": "eslint --fix src && npx stylelint --fix \"src/**/*.scss\""
+ "lint-dev": "eslint --fix src && npx stylelint --fix \"src/**/*.scss\"",
+ "lint-prod": "eslint --fix src"
},
"dependencies": {
"@monaco-editor/react": "^4.2.0",
@@ -43,8 +44,10 @@
"react-toggle": "^4.1.2",
"reactstrap": "^9.0.0",
"use-async-effect": "^2.2.3",
+ "use-debounce": "^9.0.4",
"use-interval": "^1.3.0",
"use-persisted-state": "^0.3.3",
+ "uuid": "^9.0.0",
"whatwg-fetch": "^3.6.2"
},
"engines": {
diff --git a/src/app.scss b/src/app.scss
index 068d6d6f5..1f7e08f83 100644
--- a/src/app.scss
+++ b/src/app.scss
@@ -58,6 +58,7 @@
@import 'assets/styles/components/copyable-text-holder';
@import 'assets/styles/components/badge';
@import 'assets/styles/components/code-editor';
+@import 'assets/styles/components/web-ide';
// override styles
@import 'assets/styles/overrides/dark';
diff --git a/src/assets/styles/components/_react-select.scss b/src/assets/styles/components/_react-select.scss
index 6b5dd2796..c4871b8c5 100755
--- a/src/assets/styles/components/_react-select.scss
+++ b/src/assets/styles/components/_react-select.scss
@@ -26,6 +26,15 @@
max-height: 36px;
padding-bottom: 0 !important;
padding-top: 0 !important;
+
+ .react-select__input-container {
+ .react-select__input {
+ background: transparent !important;
+ border: transparent !important;
+ outline-color: transparent !important;
+ }
+ }
+
}
}
diff --git a/src/assets/styles/components/_web-ide.scss b/src/assets/styles/components/_web-ide.scss
new file mode 100644
index 000000000..4ca091ec3
--- /dev/null
+++ b/src/assets/styles/components/_web-ide.scss
@@ -0,0 +1,987 @@
+.dark {
+ .no-projects {
+ .docs-link {
+ color: #ee81ee !important;
+ }
+ }
+
+ color: white;
+
+ .editor.current-file-path {
+ border-bottom: 1px solid transparent;
+ }
+
+ .default-window-option {
+ span {
+ color: white;
+ }
+
+ }
+
+ .name-input {
+
+ .validation-message {
+ color: $color-danger;
+ }
+
+ .name-input-container {
+ input.invalid {
+ border: 1px solid $color-danger !important;
+ outline-color: $color-danger !important;
+ }
+
+ .invalid-project-name {
+ color: $color-danger;
+ }
+ }
+ }
+
+
+ .package-install-window {
+ background: rgb(17, 17, 17);
+ }
+
+ .file.folder-selected {
+ background: rgba(200, 200, 200, 0.2) !important;
+ }
+
+ .file-menu,
+ .editor-menu {
+ border-bottom: 1px solid rgb(60, 60, 60) !important;
+ }
+
+ .file-browser button > span {
+ color: white !important;
+ }
+
+ .file-browser .file.file-selected > span {
+ color: #ee81ee !important;
+ }
+
+
+ button[disabled]:hover,
+ button[disabled] {
+ color: #5e5e5e !important;
+ }
+
+
+}
+
+.purple {
+
+
+ .default-window-option span {
+ color: white !important;
+ }
+
+ .editor-window .default-window .default-window-container {
+ background: $color-pureblack !important;
+ color: white;
+ }
+
+ .no-projects {
+ color: white;
+
+ .docs-link {
+ color: #ee81ee !important;
+ }
+ }
+
+ color: white;
+
+ .name-input {
+ .name-input-container {
+ input.invalid:focus {
+ border: 1px solid #ea4c89 !important;
+ outline-color: #ea4c89 !important;
+ }
+ }
+ .validation-message {
+ margin-top: 8px;
+ color: $color-danger;
+ }
+
+ & > input.invalid {
+ border: 1px solid $color-danger !important;
+ outline-color: $color-danger !important;
+ }
+
+
+
+ .invalid-text {
+ color: red !important;
+ }
+
+ .invalid-project-name {
+ color: $color-danger;
+ }
+ }
+
+ .editor-window .cancel-button {
+ color: rgb(105, 105, 105);;
+ }
+ .editor-window .cancel-button:hover {
+ color: white;
+ }
+
+ .package-install-window {
+ background: white;
+ }
+
+ .editor.current-file-path {
+ border-bottom: 1px solid lightgray;
+ }
+
+ color: rgb(105, 105, 105);
+
+
+ .default-window-option {
+ span {
+ color: rgb(105, 105, 105);
+ }
+ }
+
+ .cancel-button {
+ color: #5c636a;
+ }
+
+ .file.folder-selected {
+ background: rgba(200, 200, 200, 0.2) !important;
+ }
+
+ .editor-window {
+ display: flex;
+ flex-direction: column;
+
+ .current-file-path {
+ background: rgb(26, 26, 26);
+ }
+
+ /*
+ .root {
+ display: grid;
+ grid-template-columns: 150px minmax(0px, auto);
+ height: 100%;
+ }
+ */
+ // background: transparent !important;
+ }
+
+ .file-menu,
+ .editor-menu {
+ border-bottom: 1px solid darkgray !important;
+ }
+
+ .save-code[disabled] {
+ color: #c2c2c2 !important;
+ }
+
+ .file-browser button {
+ color: lightgray;
+ }
+
+ .delete-file-icon[disabled] {
+ // color: #5e5e5e !important;
+ color: #c2c2c2;
+ }
+
+
+}
+
+
+.light {
+
+ .name-input {
+
+ .validation-message {
+ color: $color-danger;
+ }
+
+ .name-input-container {
+ input.invalid {
+ border: 1px solid $color-danger !important;
+ outline-color: $color-danger !important;
+ }
+
+ .invalid-project-name {
+ color: $color-danger;
+ }
+ }
+ }
+
+
+ .no-projects {
+ color: rgb(105, 105, 105);
+ }
+
+ color: rgb(105, 105, 105);
+
+ .package-install-window {
+ background: white;
+ }
+
+ .editor.current-file-path {
+ border-bottom: 1px solid lightgray;
+ }
+
+ .restart-instance-on-save-text {
+ color: rgb(240, 240, 240) !important;
+ }
+
+ .file-menu button[disabled],
+ .editor-menu button[disabled] {
+ color: rgb(240, 240, 240) !important;
+ // #5e5e5e;
+ }
+
+ .file-browser {
+ background: #f9f9f9;
+ }
+
+ .file.folder-selected {
+ background: rgba(200, 200, 200, 0.2) !important;
+ //background: lightgray !important;
+ }
+
+ .file-menu,
+ .editor-menu {
+ border-bottom: 1px solid lightgray !important;
+ }
+
+
+ .file-menu button {
+ color: #b1b1b1;
+ }
+
+ .file-icon.filetype-unknown {
+ color: #b1b1b1 !important;
+ }
+
+ .editor-window button:hover {
+ color: white;
+ //rgb(105,105,105);
+ }
+
+ .editor-window button {
+ color: rgb(105, 105, 105);
+ }
+
+ .install-package-icon {
+ color: #f24242 !important;
+ }
+
+ .add-project-icon {
+ color: rgb(25, 168, 148) !important;
+ }
+
+ .file-menu {
+ color: rgb(105, 105, 105);
+ }
+
+ .default-window-option {
+ span {
+ color: rgb(105, 105, 105);
+ }
+ }
+}
+
+.web-ide {
+
+
+ .cancel-button {
+ margin-left: 10px;
+ }
+ // general stuff
+ height: 75vh;
+ overflow: hidden;
+
+ ul, li {
+ list-style-type: none;
+ }
+
+ label, button {
+ white-space: nowrap !important;
+ }
+
+ input {
+ padding-left: 8px;
+ padding-right: 8px;
+ }
+
+ .add-project-icon:hover,
+ .default-window .add-project-icon {
+ color: rgb(25, 168, 148); // always green
+ }
+
+
+ li {
+ margin: 0 5px 5px 0;
+ padding: 0 5px 0 0;
+ }
+
+ .default-window .install-package-icon,
+ .install-package-icon:hover {
+ color: #f24242;
+ }
+
+ .menu-pointer-icon {
+ color: rgb(143, 141, 255);
+ }
+
+ .add-folder-icon:hover,
+ .delete-folder-icon:hover {
+ color: rgb(143, 141, 255);
+ }
+
+ .delete-file-icon:hover {
+ // color: #ee81ee !important;
+ }
+
+
+ .install-package-icon,
+ .add-folder-icon,
+ .add-project-icon,
+ .upload-file-icon,
+ .delete-folder-icon,
+ .delete-file-icon,
+ .add-file-icon {
+ background: transparent;
+ border: none;
+ color: white;
+ margin: 0;
+ outline: none !important;
+ padding: 0;
+ }
+
+ .file-browser-scroll-container {
+ height: 100%;
+ overflow-y: auto;
+
+ }
+
+
+
+ // top-level element (left-side)
+ .file-browser-outer-container {
+ height: 100%;
+ overflow-y: auto;
+ }
+
+ .file-browser-inner-container {
+
+ padding: 20px;
+
+ .file-menu {
+ color: white;
+
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 20px;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-top: 0;
+
+ .file-menu-item {
+ align-items: flex-end;
+ display: flex;
+ flex-direction: row;
+ }
+ }
+
+
+ .file-browser {
+
+ margin-left: 0;
+ padding: 20px;
+ padding-left: 0;
+
+ .folder-container {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ padding-left: 0;
+
+
+ .rename-file,
+ .deploy-project {
+ margin-left: 8px;
+ }
+
+ .rename-file {
+ color: transparent;
+ }
+
+ .file {
+ &.file-selected {
+ color: #ee81ee;
+ //background: #191919 !important;
+ // background: #4a4a4a !important;
+ //border: 1px solid rgb(120,120,120);;
+ }
+
+ &.folder-selected {
+ //background: #2c2c2c !important;
+ }
+ }
+
+ .package.package-selected {
+ color: #f24242;
+ }
+
+ .project-icon {
+ color: rgb(25, 168, 148);
+
+ }
+
+ .package-icon {
+ color: #f24242;
+ }
+
+ .file,
+ .package {
+
+ background: transparent;
+ border: 1px solid transparent;
+ list-style-type: none;
+ margin: 0;
+ outline: none !important; // TODO: fix this because it is not accessible; style differently
+ padding: 3px;
+ white-space: nowrap;
+
+ &.selected {
+ }
+
+ .package-text,
+ .filename-text {
+ padding-left: 5px;
+ }
+
+ .file-icon.filetype-js {
+ color: #f7df1e;
+ }
+
+ .file-icon.filetype-yaml {
+ color: #c4c4ff;
+ }
+
+ .file-icon.filetype-unknown {
+ color: lightgray;
+ }
+
+ .folder-icon {
+ color: rgb(143 141 255);
+ }
+ }
+
+ .folder {
+ padding-left: 15px;
+ }
+
+
+ .folder.folder-contents-closed {
+ display: none;
+
+ }
+
+ .folder.folder-contents-open {
+ display: block;
+ }
+ }
+
+
+ }
+ }
+
+ // top-level element: editor window (right side)
+ // contains code editor and project management windows
+ .editor-window-container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ padding: 20px;
+
+ .editor-menu {
+ align-items: baseline;
+ border-bottom: 1px solid #2e2e2e;
+ color: white;
+ display: flex;
+ flex-direction: row;
+ justify-content: left;
+ margin-bottom: 20px;
+ margin-top: 0;
+ padding-bottom: 0;
+ padding-top: 0;
+
+ & > li {
+ margin: 0 5px 5px 0;
+ padding: 0 5px 0 0;
+ }
+
+ button[disabled],
+ button[disabled]:hover {
+ color: #5e5e5e;
+ }
+
+ .restart-instance-after-save,
+ .restart-instance {
+ background: transparent;
+ border: 1px solid transparent;
+ display: flex;
+ flex-direction: row;
+ margin: 0;
+ outline: none !important;
+ padding: 3px;
+
+ & > i {
+ color: #ee81ee;
+ }
+ }
+
+ .save-code {
+ background: transparent;
+ border: none;
+ color: #ee81ee;
+ cursor: pointer;
+ margin: 0;
+ outline: none !important;
+ padding: 0;
+ }
+
+ .save-code[disabled] {
+ color: #5e5e5e;
+ cursor: default;
+ }
+ }
+
+ .editor-window {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+
+ height: 100%;
+ justify-content: center;
+ width: 100%;
+
+
+ .name-input {
+ display: flex;
+ flex-direction: column;
+ width: 50%;
+
+
+ .validation-message {
+ margin-top: 8px;
+ }
+
+ .name-input-container {
+ display: flex;
+
+ flex-direction: row;
+ align-items: center;
+
+ .invalid-project-name {
+ margin-left: 15px;
+ }
+
+ .hidden {
+ visibility: hidden;
+ }
+ }
+
+ .name-input-buttons-container {
+ padding-top: 10px;
+ }
+ }
+
+ .delete-folder-window-title,
+ .delete-file-window-title,
+ .delete-package-window-title,
+ .folder-to-delete {
+ align-self: flex-start;
+ font-size: 1.3em;
+ font-weight: bold;
+ }
+
+ .folder-to-delete-parent-project {
+ color: $color-danger;
+ font-weight: bold;
+ }
+
+ .delete-package-window-controls {
+ .package-to-delete {
+ color: $color-danger;
+
+ font-weight: bold;
+ }
+ }
+
+
+ .delete-folder-window-controls {
+ .folder-to-delete {
+ }
+
+ .folder-to-delete-parent-project {
+ }
+ }
+
+ .delete-file-window-container {
+ display: flex;
+ flex-direction: column;
+
+ .delete-file-window-title {
+ align-self: flex-start;
+ }
+
+
+ }
+
+ .delete-file-window-controls {
+ .file-to-delete {
+ color: $color-danger;
+ font-weight: bold;
+ }
+
+
+ .file-to-delete-parent-project {
+ font-weight: bold;
+ // color: rgb(25, 168, 148);
+ // color: $color-danger;
+
+ }
+ }
+
+ .default-window {
+ .default-window-container {
+ h4 {
+ padding-bottom: 80px;
+ }
+
+ .default-window-option {
+ line-height: 2.5em;
+
+ span,
+ button > span {
+ margin-left: 20px;
+ }
+
+ span {
+ // color: white;
+ }
+
+ .add-project-button-text,
+ .install-package-button-text {
+ // color: white;
+ }
+ }
+ }
+ }
+
+
+
+ .default-window-container,
+ .name-project-window-container,
+ .name-project-folder-window-container,
+ .name-file-window-container,
+ .delete-file-window-container,
+ .delete-folder-window-container,
+ .delete-package-window-container {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ justify-content: center;
+ width: 100%;
+ }
+
+ .default-window,
+ .name-project-window,
+ .name-project-folder-window,
+ .name-file-window,
+ .delete-package-window,
+ .delete-file-window,
+ .delete-folder-window {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ justify-content: center;
+ width: 100%;
+ }
+
+ .name-project-window-title,
+ .name-file-window-title {
+ text-align: center;
+ }
+
+ .card-body {
+ }
+
+ .name-file-window {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: 100%;
+
+ .name-file-window-container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ width: 100%;
+ }
+ }
+
+ .package-install-window {
+
+ .install-package-button {
+ width: 105px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+
+ .install-package-status-icon {
+ }
+ }
+ &.github {
+ line-height:0.5em;
+ }
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ justify-content: center;
+ padding-left: 25%;
+ padding-right: 25%;
+
+ width: 100%;
+
+ .package-install-window-title {
+ font-size: 1.5em;
+ font-weight: bold;
+ text-align: center;
+ }
+
+
+ .card {
+ border: 0 solid transparent !important;
+ }
+
+ .package-install-source-form {
+ align-items: center;
+
+ display: flex;
+ flex-direction: row;
+ height: 100px;
+
+ justify-content: center;
+ padding-left: 25%;
+ padding-right: 25%;
+
+ width: 100%;
+
+
+ .package-install-option-container {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ width: 100px;
+
+
+ .package-install-source-icon {
+ font-size: 2em;
+ margin-bottom: 5px;
+ }
+
+ .package-install-source-option {
+ height: 25px;
+ width: 25px;
+ }
+ }
+ }
+
+
+ .package-install-details-form {
+ .project-name-invalid-text {
+
+ color: $color-danger !important;
+ margin-bottom: 20px;
+ display: block;
+ }
+
+
+
+ .package-install-input-container {
+ display: flex;
+ flex-direction: column;
+
+ margin-bottom: 20px;
+
+
+ .project-name-input.invalid:focus,
+ .project-name-input.invalid {
+ border: 1px solid $color-danger !important;
+ outline-color: $color-danger !important;
+ }
+
+
+ .search-status-icon-container {
+ text-align: center;
+ width: 40px;
+
+ i {
+ color: white;
+
+
+ &.not-searching {
+ color: white; //rgba(0,255,0, 0.1);
+ }
+
+
+ &.found, &.loading {
+ color: green;
+ }
+
+ &.not-found {
+ color: rgb(145, 50, 120);
+ }
+
+
+ }
+ }
+
+ .package-install-deploy-targets-dropdown {
+ }
+
+
+ .package-install-github-query-container {
+ .package-install-github-user {
+ margin-bottom: 20px;
+ }
+
+ .package-install-github-repo-lookup {
+ margin-bottom: 20px;
+
+ .github-package-search-box {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+
+ }
+ }
+ }
+
+ .package-install-npm-lookup {
+ .package-install-npm-query-container {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 20px;
+ }
+ }
+
+ }
+ }
+
+ }
+
+ .editor {
+ &.current-file-path::before {
+ }
+
+
+ &.current-file-path {
+ align-self: flex-start;
+ color: darkgray;
+
+ .current-file-path-icon {
+ padding: 5px;
+
+ }
+ }
+
+
+ width: 100%;
+ }
+
+ .package-install-source-form {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ justify-content: center;
+ width: 100%;
+
+ .package-install-source-icon {
+ font-size: 2em;
+
+ &.fab.fa-npm {
+ color: rgb(145, 50, 120);
+ font-size: 3em;
+ }
+
+ &.fa-github,
+ &.fa-link {
+ padding: 5px;
+ }
+
+ &.fa-link {
+ color: lightgray;
+ }
+
+ &.fa-github {
+ color: rgb(28, 182, 149);
+ }
+ }
+
+ .package-type {
+ border: 0 solid transparent !important;
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 30px;
+
+ label {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ min-width: 80px;
+ text-align: center;
+ }
+
+ input:focus {
+ outline: none !important;
+ }
+
+ input {
+ margin-top: 10px;
+ }
+
+ }
+ }
+
+ .filename-dialog {
+ align-items: center;
+ display: flex;
+ height: 100%;
+ justify-content: center;
+ width: 100%;
+ }
+ }
+
+ }
+}
+
+@media(max-width: 814px) {
+ .web-ide {
+ overflow: initial;
+
+ .file-browser-outer-container {
+ height: auto;
+ overflow-y: auto;
+ }
+
+ .file-browser-scroll-container {
+ max-height: 500px;
+ }
+ }
+}
diff --git a/src/assets/styles/overrides/_akamai.scss b/src/assets/styles/overrides/_akamai.scss
index 2d4f89941..5ae6703de 100644
--- a/src/assets/styles/overrides/_akamai.scss
+++ b/src/assets/styles/overrides/_akamai.scss
@@ -171,8 +171,8 @@ $partner-brand-color: #017ac6;
.fake-input,
.form-control {
background: $lightest-grey-overlay !important;
- color: $color-darkgrey !important;
border: 1px solid $color-lightgrey !important;
+ color: $color-darkgrey !important;
&.is-invalid,
&.error {
diff --git a/src/assets/styles/pages/_functions.scss b/src/assets/styles/pages/_functions.scss
index 1333018f3..c01ebf2f6 100644
--- a/src/assets/styles/pages/_functions.scss
+++ b/src/assets/styles/pages/_functions.scss
@@ -1,16 +1,2 @@
#functions {
- .entity-manager {
- .card {
- .card-body {
- &.scrollable {
- max-height: calc(27.8vh - 160px);
- min-height: 110px;
-
- &.creating {
- max-height: calc(27.8vh - 209px);
- }
- }
- }
- }
- }
}
diff --git a/src/components/auth/SignUp.js b/src/components/auth/SignUp.js
index 4ddce116b..969eed737 100644
--- a/src/components/auth/SignUp.js
+++ b/src/components/auth/SignUp.js
@@ -44,7 +44,7 @@ function SignUp() {
- Provision HarperDB Cloud and user-installed instances
- - Configure instance schemas, tables, users, roles, and clustering
+ - Configure instance databases, tables, users, roles, and clustering
- Create and monitor real-time charts based on custom queries
- Bulk data import via CSV upload or URL
diff --git a/src/components/instance/browse/EntityManager.js b/src/components/instance/browse/EntityManager.js
index 34d2e9ffd..c1ce7a8e0 100644
--- a/src/components/instance/browse/EntityManager.js
+++ b/src/components/instance/browse/EntityManager.js
@@ -1,13 +1,18 @@
import React, { useState, useEffect } from 'react';
import { Card, CardBody } from 'reactstrap';
+import { useStoreState } from 'pullstate';
import EntityManagerForm from './EntityManagerForm';
import EntityManagerRow from './EntityManagerRow';
import EntityManagerHeader from './EntityManagerHeader';
+import instanceState from '../../../functions/state/instanceState';
function EntityManager({ items, activeItem, activeSchema = false, showForm, baseUrl, itemType }) {
const [isDropping, toggleDropItem] = useState(false);
const [isCreating, toggleCreate] = useState(false);
+ const registration = useStoreState(instanceState, (s) => s.registration);
+ const [ major, minor ] = registration?.version.split('.') || [];
+ const versionAsFloat = parseFloat(`${major}.${minor}`);
useEffect(() => {
toggleCreate();
@@ -58,7 +63,7 @@ function EntityManager({ items, activeItem, activeSchema = false, showForm, base
) : items && !items.length && !showForm ? (
- no visible schemas or tables
+ no visible { versionAsFloat >= 4.2 ? 'databases' : 'schemas' } or tables
) : null}
diff --git a/src/components/instance/browse/index.js b/src/components/instance/browse/index.js
index 4aea83fa8..0f3368e02 100644
--- a/src/components/instance/browse/index.js
+++ b/src/components/instance/browse/index.js
@@ -38,6 +38,10 @@ function BrowseIndex() {
const [instanceAuths] = useInstanceAuth({});
const auth = instanceAuths && instanceAuths[compute_stack_id];
const url = useStoreState(instanceState, (s) => s.url);
+ const registration = useStoreState(instanceState, (s) => s.registration);
+ const version = registration?.version;
+ const [major, minor] = version?.split('.') || [];
+ const versionAsFloat = `${major}.${minor}`;
const structure = useStoreState(instanceState, (s) => s.structure);
const [entities, setEntities] = useState({ schemas: [], tables: [], activeTable: false });
const [tableState, setTableState] = useState(defaultTableState);
@@ -45,7 +49,7 @@ function BrowseIndex() {
const showForm = instanceAuths[compute_stack_id]?.super || instanceAuths[compute_stack_id]?.structure === true;
const showTableForm = showForm || (instanceAuths[compute_stack_id]?.structure && instanceAuths[compute_stack_id]?.structure?.includes(schema));
const emptyPromptMessage = showForm
- ? `Please ${(schema && entities.tables && !entities.tables.length) || !entities.schemas.length ? 'create' : 'choose'} a ${schema ? 'table' : 'schema'}`
+ ? `Please ${(schema && entities.tables && !entities.tables.length) || !entities.schemas.length ? 'create' : 'choose'} a ${schema ? 'table' : `${versionAsFloat >= 4.2 ? 'database' : 'schema'}` }`
: "This user has not been granted access to any tables. A super-user must update this user's role.";
const syncInstanceStructure = () => {
@@ -100,17 +104,34 @@ function BrowseIndex() {
return (
- addError({ error: { message: error.message, componentStack } })} FallbackComponent={ErrorFallback}>
-
- {schema && }
-
+ addError({ error: { message: error.message, componentStack } })}
+ FallbackComponent={ErrorFallback}>
+ = 4.2 ? 'database' : 'schema'}
+ showForm={showForm} />
+ {
+ schema &&
+ }
+ = 4.2 ? 'databases' : 'schemas'} and tables`} />
- addError({ error: { message: error.message, componentStack } })} FallbackComponent={ErrorFallback}>
+ addError({ error: { message: error.message, componentStack } })}
+ FallbackComponent={ErrorFallback}>
{schema && table && action === 'csv' && entities.activeTable ? (
) : schema && table && action && entities.activeTable ? (
diff --git a/src/components/instance/functions/manage/CustomFunctionsEditor.js b/src/components/instance/functions/manage/CustomFunctionsEditor.js
new file mode 100644
index 000000000..cb530b03f
--- /dev/null
+++ b/src/components/instance/functions/manage/CustomFunctionsEditor.js
@@ -0,0 +1,128 @@
+import React, { useEffect } from 'react';
+import { Row, Col } from 'reactstrap';
+import { ErrorBoundary } from 'react-error-boundary';
+import { useStoreState } from 'pullstate';
+import { useParams, useNavigate } from 'react-router-dom';
+import useInterval from 'use-interval';
+
+import instanceState from '../../../../functions/state/instanceState';
+
+import EntityManager from './EntityManager';
+import CodeEditor from './CodeEditor';
+import Deploy from './Deploy';
+import EmptyPrompt from '../../../shared/EmptyPrompt';
+import ErrorFallback from '../../../shared/ErrorFallback';
+import addError from '../../../../functions/api/lms/addError';
+import EntityReloader from './EntityReloader';
+import StaticEntityStatus from './StaticEntityStatus';
+import CopyableText from '../../../shared/CopyableText';
+
+function CustomFunctionsEditor({ refreshCustomFunctions, loading }) {
+ const { customer_id, compute_stack_id, action = 'edit', project, file } = useParams();
+ const navigate = useNavigate();
+ const custom_functions = useStoreState(instanceState, (s) => s.custom_functions);
+ const registration = useStoreState(instanceState, (s) => s.registration);
+ const [majorVersion, minorVersion] = (registration?.version || '').split('.');
+ const supportsStaticRoutes = parseFloat(`${majorVersion}.${minorVersion}`) < 4.1;
+ const restarting = useStoreState(instanceState, (s) => s.restarting_service === 'custom_functions');
+ const cf_server_url = useStoreState(instanceState, (s) => s.custom_functions_url || `${s.url.split(':').slice(0, -1).join(':')}:${s.custom_functions?.port}`);
+ const baseUrl = `/o/${customer_id}/i/${compute_stack_id}/functions/${action}`;
+
+ useEffect(() => {
+ const hasProjects = custom_functions?.endpoints && Object.keys(custom_functions?.endpoints).length;
+ const projectIsInEndpoints = custom_functions?.endpoints && Object.keys(custom_functions?.endpoints).includes(project);
+
+ if (hasProjects && project && !projectIsInEndpoints) {
+ const firstProject = project && Object.keys(custom_functions?.endpoints)[0];
+ navigate(`${baseUrl}/${firstProject}`);
+ } else if (hasProjects && project && !file) {
+ const firstRouteFile = project && custom_functions?.endpoints[project]?.routes[0];
+ const firstHelperFile = project && custom_functions?.endpoints[project]?.helpers[0];
+ const defaultType = firstRouteFile ? 'routes' : 'helpers';
+ navigate(`${baseUrl}/${project}/${defaultType}/${firstRouteFile || firstHelperFile}`);
+ } else if (hasProjects && !project) {
+ const firstProject = Object.keys(custom_functions?.endpoints)[0];
+ navigate(`${baseUrl}/${firstProject}`);
+ } else if (!hasProjects) {
+ navigate(baseUrl);
+ }
+ }, [custom_functions?.endpoints, customer_id, compute_stack_id, navigate, action, project, file, baseUrl]);
+
+ useInterval(async () => {
+ if (cf_server_url && restarting) {
+ try {
+ await fetch(cf_server_url);
+ instanceState.update((s) => {
+ s.restarting_service = false;
+ });
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.log(e);
+ }
+ }
+ }, 1000);
+
+ return (
+
+
+ addError({ error: { message: error.message, componentStack } })} FallbackComponent={ErrorFallback}>
+
+ {project && action === 'edit' && (
+ <>
+
+
+ { supportsStaticRoutes && }
+
+ >
+ )}
+
+
+
Root File Directory
+
+
Custom Functions Server URL
+
+
+
+
+
+
+
+ {action === 'deploy' ? (
+
+ ) : project ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+export default CustomFunctionsEditor;
diff --git a/src/components/instance/functions/manage/index.js b/src/components/instance/functions/manage/index.js
index 0ab0c70c7..5c5ca5889 100644
--- a/src/components/instance/functions/manage/index.js
+++ b/src/components/instance/functions/manage/index.js
@@ -1,144 +1,447 @@
-import React, { useEffect } from 'react';
-import { Row, Col } from 'reactstrap';
-import { ErrorBoundary } from 'react-error-boundary';
+import React, { useState } from 'react';
import { useStoreState } from 'pullstate';
-import { useParams, useNavigate } from 'react-router-dom';
-import useInterval from 'use-interval';
+import { useParams } from 'react-router';
+import { useAlert } from 'react-alert';
+import appState from '../../../../functions/state/appState';
import instanceState from '../../../../functions/state/instanceState';
+import getComponentFile from '../../../../functions/api/instance/getComponentFile';
+import setComponentFile from '../../../../functions/api/instance/setComponentFile';
+import addComponent from '../../../../functions/api/instance/addComponent';
+import packageComponent from '../../../../functions/api/instance/packageComponent';
+import dropComponent from '../../../../functions/api/instance/dropComponent';
+import deployComponent from '../../../../functions/api/instance/deployComponent';
+import restartInstance from '../../../../functions/api/instance/restartInstance';
-import EntityManager from './EntityManager';
-import CodeEditor from './CodeEditor';
-import Deploy from './Deploy';
-import EmptyPrompt from '../../../shared/EmptyPrompt';
-import ErrorFallback from '../../../shared/ErrorFallback';
-import addError from '../../../../functions/api/lms/addError';
-import EntityReloader from './EntityReloader';
-import StaticEntityStatus from './StaticEntityStatus';
-import CopyableText from '../../../shared/CopyableText';
+import useInstanceAuth from '../../../../functions/state/instanceAuths';
+
+import ApplicationsEditor from '../../../shared/webide';
+import CustomFunctionsEditor from './CustomFunctionsEditor';
+
+/*
+ * Parse the relative path, which is what the components api expects.
+ *
+ * note: While 'components' root is not shown in the app,
+ * the full path is components//relative/path/to/file.js
+ */
+
+function getRelativeFilepath(absolutePath) {
+ // for saving to server, which requires skipping root /components dir
+ // and treating /components/projects dir as project name.
+ // filepath is /path/to/file.js
+
+ return absolutePath.split('/').slice(2).join('/');
+
+}
+
+function getDeployTargets(instanceList, instanceAuthList, thisCsId, auth) {
+
+ return instanceList.reduce((memo, instance) => {
+
+ const csId = instance.compute_stack_id;
+ const deployTarget = instanceAuthList[csId];
+
+ if (!deployTarget?.version) {
+ return memo;
+ }
+
+ const [ major, minor ] = deployTarget?.version.split('.') || [];
+
+ // exclude < 4.2
+
+ if (parseInt(major, 10) >= 4 && parseInt(minor, 10) >= 2) {
+
+ memo.push({
+ isCurrentInstance: csId === thisCsId,
+ auth,
+ instance
+ });
+
+ }
+
+ return memo;
+
+ }, []);
+
+}
function ManageIndex({ refreshCustomFunctions, loading }) {
- const { customer_id, compute_stack_id, action = 'edit', project, file } = useParams();
- const navigate = useNavigate();
- const custom_functions = useStoreState(instanceState, (s) => s.custom_functions);
+
+ const { compute_stack_id } = useParams();
const registration = useStoreState(instanceState, (s) => s.registration);
- const [majorVersion, minorVersion] = (registration?.version || '').split('.');
- const supportsStaticRoutes = parseFloat(`${majorVersion}.${minorVersion}`) < 4.1;
- const restarting = useStoreState(instanceState, (s) => s.restarting_service === 'custom_functions');
- // NOTE: use the operationsApi server for cf_server_url in the future.
- const cf_server_url = useStoreState(instanceState, (s) => s.custom_functions_url || `${s.url.split(':').slice(0, -1).join(':')}:${s.custom_functions?.port}`);
- const baseUrl = `/o/${customer_id}/i/${compute_stack_id}/functions/${action}`;
+ 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 theme = useStoreState(appState, (s) => s.theme);
+ const [ restartingInstance, setRestartingInstance ] = useState(false);
+ const alert = useAlert();
+
+ async function restartWithLoadingState({ auth: instanceAuth, url: instanceUrl }) {
+
+ setRestartingInstance(true);
+
+ setTimeout(async () => {
+ await restartInstance({ auth: instanceAuth, url: instanceUrl });
+ setRestartingInstance(false);
+ }, 100);
+
+ }
+
+ // save file to instance
+ async function saveCodeToInstance(selectedFile, restartRequired) {
+
+ const filepathRelativeToProjectDir = selectedFile.path.split('/').slice(2).join('/');
+ const payload = {
+ auth,
+ url,
+ project: selectedFile.project,
+ file: filepathRelativeToProjectDir,
+ payload: selectedFile.content
+ };
+
+ const { error, message } = await setComponentFile(payload);
+
+ if (error) {
+ alert.error(message);
+ }
+
+
+ if (restartRequired) {
+ await restartWithLoadingState({ auth, url });
+ }
+
+ await refreshCustomFunctions();
+
+ }
+
+ // eslint-disable-next-line no-empty-function
+ async function renameFolder() {
+ }
+
+ async function renameFile(newFileName, info) {
+
+ const { path, content, project } = info;
+ const parentDir = getRelativeFilepath(path).split('/').slice(0, -1).join('/');
+ const newFilenameRelativePath = parentDir ? `${parentDir}/${newFileName}` : newFileName;
+
+ // TODO: error handling
+ await dropComponent({
+ auth,
+ url,
+ project,
+ file: getRelativeFilepath(path)
+ });
+
+ await setComponentFile({
+ auth,
+ url,
+ project,
+ file: newFilenameRelativePath,
+ payload: content
+ })
+
+ refreshCustomFunctions();
+
+ }
+
+ async function selectNewFile(selectedFile) {
+
+ const { path, project, name } = selectedFile;
+ const newFile = getRelativeFilepath(path);
+ const { error, message } = await getComponentFile({
+ auth,
+ url,
+ project,
+ file: newFile
+ });
+
+ if (error) {
+
+ alert.error(message);
+
+ return {
+ content: '',
+ path,
+ project,
+ name
+ };
+
+ }
+
+ return {
+ content: message,
+ path,
+ project,
+ name
+ };
+
+ }
+
+ async function deleteFile(f) {
+
+ const { error, message } = await dropComponent({
+ auth,
+ url,
+ project: f.project,
+ file: getRelativeFilepath(f.path)
+ });
+
+ if (error) {
+ alert.error(message);
+ }
+
+ await refreshCustomFunctions();
+
+ }
- const routeToDefaultProject = () => {
+ async function deletePackage({ name: project }) {
- const hasProjects = custom_functions?.endpoints && Object.keys(custom_functions?.endpoints).length;
- const projectIsInEndpoints = custom_functions?.endpoints && Object.keys(custom_functions?.endpoints).includes(project);
+ const { error, message } = await dropComponent({
+ auth,
+ url,
+ project
+ });
- let targetUrl;
+ if (error) {
+ alert.error(message);
+ return;
+ }
+
+ await restartInstance({ auth, url });
+ await refreshCustomFunctions();
+
+ }
+
+ async function deleteFolder({ path, project }) {
+
+
+ const targetDirpath = getRelativeFilepath(path);
+
+ // if we're deleting a top-level directory, that's a project,
+ // so don't pass a file. otherwise pass project name and file/dir
+ // relative to project name as 'file'.
+
+ if (targetDirpath.length > 0) {
+
+ const { error, message } = await dropComponent({
+ auth,
+ url,
+ project,
+ file: targetDirpath
+ });
+
+ if (error) {
+ alert.error(message);
+ }
+
+ } else {
+
+ const { error, message } = await dropComponent({
+ auth,
+ url,
+ project
+ });
+
+ if (error) {
+ alert.error(message);
+ }
- if (hasProjects && project && !projectIsInEndpoints) {
+ }
+
+ await refreshCustomFunctions();
+
+ }
+
+ async function createNewProject(newProjectName) {
+
+ const { error, message } = await addComponent({
+ auth,
+ url,
+ project: newProjectName
+ });
+
+
+ if (error) {
+ alert.error(message);
+ }
- const firstProject = project && Object.keys(custom_functions?.endpoints)[0];
- targetUrl = `${baseUrl}/${firstProject}`;
+ await restartInstance({ auth, url });
- } else if (hasProjects && project && !file) {
+ await refreshCustomFunctions();
- const firstRouteFile = project && custom_functions?.endpoints[project]?.routes[0];
- const firstHelperFile = project && custom_functions?.endpoints[project]?.helpers[0];
- const defaultType = firstRouteFile ? 'routes' : 'helpers';
- targetUrl = `${baseUrl}/${project}/${defaultType}/${firstRouteFile || firstHelperFile}`;
+ }
- } else if (hasProjects && !project) {
+ async function createNewProjectFolder(newFolderName, parentFolder) {
- const firstProject = Object.keys(custom_functions?.endpoints)[0];
- targetUrl = `${baseUrl}/${firstProject}`;
+ const { path, project } = parentFolder;
+ const relativeDirpath = getRelativeFilepath(path);
+ const relativeFilepath = relativeDirpath ? `${relativeDirpath}/${newFolderName}` : newFolderName;
- } else if (!hasProjects) {
+ const { error, message } = await setComponentFile({
+ auth,
+ url,
+ project,
+ file: relativeFilepath
+ })
- targetUrl = baseUrl;
+ if (error) {
+ alert.error(message);
}
- navigate(targetUrl);
+ await refreshCustomFunctions();
+
}
- const waitForRestartToComplete = async () => {
- if (cf_server_url && restarting) {
- try {
- await fetch(cf_server_url);
- instanceState.update((s) => {
- s.restarting_service = false;
- });
- } catch (e) {
- // eslint-disable-next-line no-console
- console.log(e);
+ async function installPackage(projectName, packageUrl, deployTargets) {
+
+ const deployPromises = deployTargets.map(async (t) => {
+
+ // TODO: check error here.
+
+ const { error, message } = await deployComponent({
+
+ auth: t.auth,
+ url: t.instance.url,
+ project: projectName,
+ packageUrl
+ });
+
+ if (error) {
+ alert.error(message);
}
+
+ // TODO: what do we actually want to do about an invalid package?
+ // change to restartService({ auth, url, service: 'http_worker' });
+ await restartInstance({ auth: t.auth, url: t.instance.url });
+
+ });
+
+
+ await Promise.all(deployPromises);
+ await refreshCustomFunctions();
+
+ /*
+ for (const deployTarget of deployTargets) {
+
+ // TODO: check error here.
+ await deployComponent({
+ auth: deployTarget.auth,
+ url: deployTarget.instance.url,
+ project: projectName,
+ packageUrl
+ });
+
+ // TODO: what do we actually want to do about an invalid package?
+ // change to restartService({ auth, url, service: 'http_worker' });
+
+ await restartInstance({ auth: deployTarget.auth, url: deployTarget.instance.url });
+
}
+ */
+
+ await refreshCustomFunctions();
+
+
}
- useEffect(routeToDefaultProject, [custom_functions?.endpoints, customer_id, compute_stack_id, navigate, action, project, file, baseUrl]);
- useInterval(waitForRestartToComplete, 1000);
-
- return (
-
-
- addError({ error: { message: error.message, componentStack } })} FallbackComponent={ErrorFallback}>
-
- {project && action === 'edit' && (
- <>
-
-
- { supportsStaticRoutes && }
-
- >
- )}
-
-
-
Root File Directory
-
-
Custom Functions Server URL
-
-
-
-
-
-
-
- {action === 'deploy' ? (
-
- ) : project ? (
-
- ) : (
-
- )}
-
-
- );
+ async function createNewFile(newFilename, parentFolder) {
+
+ const { path, project } = parentFolder;
+ const relativeDirpath = getRelativeFilepath(path);
+ const relativeFilepath = relativeDirpath ? `${relativeDirpath}/${newFilename}` : newFilename;
+ const payload = '';
+
+ // NOTE: to server, path is everything after '/components/project' in /components/project/path/to/file.js
+ // in IDE land, path is full, aka, 'components/project/path/to/file.js'.
+
+ await setComponentFile({
+ auth,
+ url,
+ project,
+ file: relativeFilepath,
+ payload
+ });
+
+ await refreshCustomFunctions();
+
+ return {
+ content: payload,
+ path: [parentFolder.path, newFilename].join('/'),
+ project,
+ };
+
+ }
+
+ async function deployProject({ project, deployTarget }) {
+
+ const {
+ auth: otherInstanceAuth,
+ instance: otherInstance
+ } = deployTarget;
+
+ const { payload } = await packageComponent({
+ auth,
+ url,
+ project: project.name
+ });
+
+ // deploy to targetInstance
+ await deployComponent({
+ auth: otherInstanceAuth,
+ url: otherInstance.url,
+ project: project.name,
+ payload
+ })
+
+ // restart targetInstance
+ await restartInstance({
+ auth: otherInstanceAuth,
+ url: otherInstance.url
+ });
+
+ // restart this instance
+ await restartInstance({
+ auth,
+ url
+ });
+
+ await refreshCustomFunctions();
+
+ }
+
+ return supportsApplicationsAPI ?
+ restartWithLoadingState({auth, url}) }
+ restartingInstance={restartingInstance}
+ />
+ :
+
+
}
export default ManageIndex;
diff --git a/src/components/instance/index.js b/src/components/instance/index.js
index fdd78b361..76c0f16df 100644
--- a/src/components/instance/index.js
+++ b/src/components/instance/index.js
@@ -36,7 +36,8 @@ function InstanceIndex() {
const thisInstance = useStoreState(appState, (s) => compute_stack_id && s.instances && s.instances.find((i) => i.compute_stack_id === compute_stack_id), [compute_stack_id]);
const url = useStoreState(instanceState, (s) => s.url);
const restarting = useStoreState(instanceState, (s) => s.restarting);
- const hydratedRoutes = routes({ customer_id, super_user: instanceAuth?.super });
+ const registration = useStoreState(instanceState, (s) => s.registration);
+ const hydratedRoutes = routes({ customer_id, super_user: instanceAuth?.super, version: registration?.version });
const [mounted, setMounted] = useState(false);
useAsyncEffect(() => {
diff --git a/src/components/instance/roles/JsonViewer.js b/src/components/instance/roles/JsonViewer.js
index 73b1727a0..9bf339574 100644
--- a/src/components/instance/roles/JsonViewer.js
+++ b/src/components/instance/roles/JsonViewer.js
@@ -22,6 +22,8 @@ function JsonViewer({ showAttributes, fetchRoles }) {
const { role_id } = useParams();
const roles = useStoreState(instanceState, (s) => s.roles);
const version = useStoreState(instanceState, (s) => s.registration?.version);
+ const [ major, minor ] = version?.split('.') || [];
+ const versionAsFloat = parseFloat(`${major}.${minor}`);
const lastUpdate = useStoreState(instanceState, (s) => s.lastUpdate);
const auth = useStoreState(instanceState, (s) => s.auth);
const url = useStoreState(instanceState, (s) => s.url);
@@ -33,13 +35,12 @@ function JsonViewer({ showAttributes, fetchRoles }) {
const [loading, setLoading] = useState(false);
const [changed, setChanged] = useState(false);
const [validJSON, setValidJSON] = useState(true);
- const [major, minor] = version.split('.');
const hasStructureUser = major >= 3 && (major > 3 || minor >= 3);
const jsonHeight = hasStructureUser ? "calc(100vh - 440px)" : "calc(100vh - 340px)"
const icons = {
- checked: manage schemas/tables
,
- unchecked: manage schemas/tables
,
+ checked: manage { versionAsFloat >= 4.2 ? 'databases' : 'schemas' }/tables
,
+ unchecked: manage { versionAsFloat >= 4.2 ? 'databases' : 'schemas' }/tables
,
};
useAsyncEffect(async () => {
@@ -113,8 +114,8 @@ function JsonViewer({ showAttributes, fetchRoles }) {
}}
type="text"
id="permitted_schemas"
- title="permitteed schemas"
- placeholder="comma-separated schemas, or blank for all"
+ title={`permitted ${ versionAsFloat >= 4.2 ? 'databases' : 'schemas' }`}
+ placeholder={`comma-separated ${ versionAsFloat >= 4.2 ? 'databases' : 'schemas' }, or blank for all`}
value={newStructureUser === true ? '' : newStructureUser}
disabled={loading}
/>
diff --git a/src/components/instance/roles/index.js b/src/components/instance/roles/index.js
index d9280aa27..eae8e9cef 100644
--- a/src/components/instance/roles/index.js
+++ b/src/components/instance/roles/index.js
@@ -38,6 +38,9 @@ function RolesIndex() {
const [loading, setLoading] = useState(false);
const [formState, setFormState] = useState(defaultState);
const baseUrl = `/o/${customer_id}/i/${compute_stack_id}/roles`;
+ const version = useStoreState(instanceState, (s) => s.registration?.version);
+ const [ major, minor ] = version?.split('.') || [];
+ const versionAsFloat = parseFloat(`${major}.${minor}`);
const fetchRoles = useCallback(async () => {
setLoading(true);
@@ -109,7 +112,7 @@ function RolesIndex() {
{role_id ? (
- Super Users and Cluster Users have full access to all schemas, tables, and attributes.
+ Super Users and Cluster Users have full access to all { versionAsFloat >= 4.2 ? 'databases' : 'schemas'}, tables, and attributes.
) : (
Please choose or add a role to manage it.
)}
diff --git a/src/components/instance/routes.js b/src/components/instance/routes.js
index d58bd0c15..f5a76e824 100644
--- a/src/components/instance/routes.js
+++ b/src/components/instance/routes.js
@@ -75,6 +75,15 @@ const functions = {
iconCode: 'f542',
};
+const applications = {
+ element: ,
+ path: 'applications',
+ link: 'applications',
+ label: 'applications',
+ icon: 'project-diagram',
+ iconCode: 'f542',
+};
+
const metrics = {
element: ,
path: `status`,
@@ -102,6 +111,26 @@ const examples = {
iconCode: 'f121',
};
-const routes = ({ super_user }) => (super_user ? [browse, query, users, roles, charts, cluster, functions, metrics, config, examples] : [browse, query, charts, examples]);
+const routes = ({ super_user, version=null }) => {
+
+ let supportsApplications = false;
+
+ if (version) {
+
+ const [ a, b ] = version?.split('.') || [];
+
+ const major = parseInt(a, 10);
+ const minor = parseInt(b, 10);
+ const versionAsFloat = parseFloat(`${major}.${minor}`);
+
+ supportsApplications = versionAsFloat >= 4.2;
+
+ }
+ if (super_user) {
+ return [browse, query, users, roles, charts, cluster, supportsApplications ? applications : functions, metrics, config, examples];
+ }
+ return [browse, query, charts, examples];
+
+}
export default routes;
diff --git a/src/components/shared/webide/Editor.js b/src/components/shared/webide/Editor.js
new file mode 100644
index 000000000..67a08411c
--- /dev/null
+++ b/src/components/shared/webide/Editor.js
@@ -0,0 +1,66 @@
+import React, { useState, useEffect } from 'react';
+import ReactMonacoEditor from '@monaco-editor/react';
+
+function parseFileExtension(filename) {
+ const parts = (filename || '')?.split('.');
+ return parts.length > 1 ? parts.slice(-1)[0] : '';
+}
+
+const extensionToLanguageMap = {
+ js: 'javascript',
+ yaml: 'yaml',
+ ts: 'typescript',
+ json: 'json',
+ md: 'markdown',
+ html: 'html',
+ css: 'css',
+ graphql: 'graphql'
+};
+
+// TODO: update code using whatever monaco hook is available. onupdate.
+// don't allow save if there are errors.
+function Editor({ active, file, onChange, theme }) {
+
+ const [ language, setLanguage ] = useState('javascript');
+ const [ editorTheme, setEditorTheme ] = useState('vs-dark');
+
+ useEffect(() => {
+ const extension = parseFileExtension(file?.name);
+ const updatedLanguage = extensionToLanguageMap[extension] || 'plaintext';
+ setLanguage(updatedLanguage);
+ }, [file]);
+
+ useEffect(() => {
+ setEditorTheme(theme === 'light' ? 'light' : 'vs-dark');
+ }, [theme]);
+
+
+ if (!active) {
+ return null;
+ }
+
+ const filepathRelativeToComponentsDir = file.path.split('/').slice(1).join('/');
+
+ // eslint-disable-next-line no-unused-vars
+ return <>
+
+
+ {filepathRelativeToComponentsDir}
+
+
+ >
+}
+
+export default Editor;
diff --git a/src/components/shared/webide/EditorMenu.js b/src/components/shared/webide/EditorMenu.js
new file mode 100644
index 000000000..3f09991d7
--- /dev/null
+++ b/src/components/shared/webide/EditorMenu.js
@@ -0,0 +1,91 @@
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+import React, { useState } from 'react';
+import cn from 'classnames';
+
+export function SaveButton({ disabled, onSave }) {
+
+ const [ loading, setLoading ] = useState(false);
+ return (
+