}>
-
+
{fetchingUser ? (
diff --git a/src/components/LocalApp.js b/src/components/LocalApp.js
new file mode 100644
index 000000000..fdd2d2b56
--- /dev/null
+++ b/src/components/LocalApp.js
@@ -0,0 +1,72 @@
+import React, { useEffect, useState, lazy, Suspense } from 'react';
+import { Route, Routes, Navigate, useNavigate, useLocation } from 'react-router-dom';
+import { ErrorBoundary } from 'react-error-boundary';
+
+import usePersistedUser from '../functions/state/persistedUser';
+import config from '../config';
+
+import Loader from './shared/Loader';
+import ErrorFallback from './shared/ErrorFallback';
+import ErrorFallbackAuth from './shared/ErrorFallbackAuth';
+
+import init from '../functions/app/init';
+import changeFavIcon from '../functions/app/changeFavIcon';
+import useInstanceAuth from '../functions/state/instanceAuths';
+
+const TopNav = lazy(() => import(/* webpackChunkName: "topnav" */ './TopNav'));
+const Instance = lazy(() => import(/* webpackChunkName: "instance" */ './instance'));
+const SignIn = lazy(() => import(/* webpackChunkName: "signIn" */ './auth/LocalSignIn'));
+
+let controller;
+
+function LocalApp() {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const [fetchingUser, setFetchingUser] = useState(true);
+ const [persistedUser, setPersistedUser] = usePersistedUser({});
+ const [instanceAuths] = useInstanceAuth({});
+
+ useEffect(() => {
+ changeFavIcon(persistedUser?.theme);
+ }, [persistedUser?.theme]);
+
+ useEffect(() => {
+ init({ currentPath: location.pathname, navigate, persistedUser, setPersistedUser, setFetchingUser, instanceAuths, controller });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
+ }>
+
+
+ {fetchingUser ? (
+
+ ) : instanceAuths?.local?.valid ? (
+
+ }>
+ {/* can we put instance routes in here, each in a suspense tag (since they're lazily loaded) */}
+
+ } path="/o/:customer_id/i/:compute_stack_id/*" />
+ } />
+
+
+
+ ) : (
+
+ }>
+
+ } path="/" />
+
+
+
+ )}
+
+
+
HarperDB Local Studio v{config.studio_version}
+
+ );
+}
+
+export default LocalApp;
diff --git a/src/components/TopNav.js b/src/components/TopNav.js
index 18afc2f77..7339d4eb4 100644
--- a/src/components/TopNav.js
+++ b/src/components/TopNav.js
@@ -7,39 +7,49 @@ import { ErrorBoundary } from 'react-error-boundary';
import appState from '../functions/state/appState';
import addError from '../functions/api/lms/addError';
import ErrorFallback from './shared/ErrorFallback';
+import useInstanceAuth from '../functions/state/instanceAuths';
+import config from '../config';
+import usePersistedUser from '../functions/state/persistedUser';
-function TopNav({ isMaintenance }) {
+function TopNav({ isMaintenance, loggedIn = false }) {
const { pathname } = useLocation();
const navigate = useNavigate();
const auth = useStoreState(appState, (s) => s.auth);
const customer = useStoreState(appState, (s) => s.customer);
const theme = useStoreState(appState, (s) => s.theme);
const themes = useStoreState(appState, (s) => s.themes);
+ const [, setInstanceAuths] = useInstanceAuth({});
+ const [persistedUser, setPersistedUser] = usePersistedUser({});
const themeIndex = themes?.findIndex((t) => t === theme);
const nextTheme = !theme || themeIndex === themes.length - 1 ? themes[0] : themes[themeIndex + 1];
- const loggedIn = auth?.user_id;
const showInviteBadge = useMemo(
() => auth?.orgs?.filter((org) => org.status === 'invited').length,
// eslint-disable-next-line react-hooks/exhaustive-deps
- [auth.orgs]
+ [auth.orgs],
);
const showManageIcon = useMemo(
() => auth?.orgs?.find((o) => o.customer_id?.toString() === customer?.customer_id?.toString())?.status === 'owner',
// eslint-disable-next-line react-hooks/exhaustive-deps
- [auth.orgs, customer.customer_id]
+ [auth.orgs, customer.customer_id],
);
- const toggleTheme = (newValue) =>
+ const toggleTheme = (newValue) => {
+ setPersistedUser({ ...persistedUser, theme: newValue });
appState.update((s) => {
s.theme = newValue;
});
+ };
const logOut = () => {
- appState.update((s) => {
- s.auth = false;
- });
+ if (config.is_local_studio) {
+ setInstanceAuths({ local: false });
+ } else {
+ appState.update((s) => {
+ s.auth = false;
+ });
+ }
navigate('/');
};
@@ -56,17 +66,17 @@ function TopNav({ isMaintenance }) {
>
- {loggedIn && !isMaintenance && (
+ {loggedIn && !isMaintenance && !config.is_local_studio && (
<>
-
+
all organizations
{showInviteBadge ? {showInviteBadge} : null}
@@ -90,7 +100,7 @@ function TopNav({ isMaintenance }) {
-
+
billing
{customer?.current_payment_status?.status === 'invoice.payment_failed' ? ! : null}
@@ -108,16 +118,14 @@ function TopNav({ isMaintenance }) {
>
)}
-
-
-
- docs
-
-
+ {!config.is_local_studio && (
+
+
+
+ docs
+
+
+ )}
{themes.length > 1 && (
{loggedIn ? (
e.keyCode !== 13 || logOut()} onClick={logOut}>
-
+
sign out
) : (
-
+
sign in
)}
diff --git a/src/components/auth/LocalSignIn.js b/src/components/auth/LocalSignIn.js
new file mode 100644
index 000000000..aadb2d3c6
--- /dev/null
+++ b/src/components/auth/LocalSignIn.js
@@ -0,0 +1,106 @@
+import React, { useState } from 'react';
+import { Card, CardBody, Form, Input, Button } from 'reactstrap';
+import { useNavigate } from 'react-router-dom';
+import { useStoreState } from 'pullstate';
+
+import Loader from '../shared/Loader';
+import useInstanceAuth from '../../functions/state/instanceAuths';
+import userInfo from '../../functions/api/instance/userInfo';
+import appState from '../../functions/state/appState';
+
+function SignIn() {
+ const url = useStoreState(appState, (s) => s.instances.find((i) => i.compute_stack_id === 'local')?.url);
+ const [formState, setFormState] = useState({});
+ const [formData, setFormData] = useState({});
+ const [instanceAuths, setInstanceAuths] = useInstanceAuth({});
+ const navigate = useNavigate();
+
+ const submit = async () => {
+ setFormState({ submitted: true });
+ const { user, pass } = formData;
+ if (!user || !pass) {
+ setFormState({ error: 'All fields are required' });
+ } else {
+ const result = await userInfo({ auth: { user, pass }, url });
+
+ if (result.error && result.type === 'catch') {
+ setFormState({ error: 'SSL ERROR. ACCEPT SELF-SIGNED CERT?', url });
+ } else if ((result.error && result.message === 'Login failed') || result.error === 'Login failed') {
+ setFormState({ error: 'Login failed. Using instance credentials?', url: 'https://harperdb.io/docs/harperdb-studio/instances/#instance-login' });
+ } else if (result.error) {
+ setFormState({ error: result.message || result.error });
+ } else {
+ setInstanceAuths({
+ ...instanceAuths,
+ local: { valid: true, user: formData.user, pass: formData.pass, super: result.role.permission.super_user, structure: result.role.permission.structure_user },
+ });
+ setTimeout(navigate('/o/local/i/local/browse'), 0);
+ }
+ }
+ };
+
+ const keyDown = (e) => {
+ if (e.code === 'Enter') {
+ setFormState({ submitted: true });
+ }
+ };
+
+ return (
+
+ );
+}
+
+export default SignIn;
diff --git a/src/components/auth/SignIn.js b/src/components/auth/SignIn.js
index 78c260fa9..cc619b034 100644
--- a/src/components/auth/SignIn.js
+++ b/src/components/auth/SignIn.js
@@ -9,6 +9,7 @@ import appState from '../../functions/state/appState';
import getUser from '../../functions/api/lms/getUser';
import isEmail from '../../functions/util/isEmail';
import Loader from '../shared/Loader';
+import usePersistedUser from '../../functions/state/persistedUser';
function SignIn() {
const { search } = useLocation();
@@ -17,6 +18,7 @@ function SignIn() {
const theme = useStoreState(appState, (s) => s.theme);
const [formState, setFormState] = useState({});
const [formData, setFormData] = useState({});
+ const [persistedUser, setPersistedUser] = usePersistedUser({});
const submit = () => {
setFormState({ submitted: true });
@@ -29,6 +31,7 @@ function SignIn() {
setFormState({ error: 'portal access denied' });
} else {
setFormState({ processing: true });
+ setPersistedUser({ ...persistedUser, email, pass });
getUser({ email, pass, loggingIn: true });
}
};
@@ -43,7 +46,7 @@ function SignIn() {
useEffect(
() => !formState.submitted && setFormState({}),
// eslint-disable-next-line react-hooks/exhaustive-deps
- [formData]
+ [formData],
);
useEffect(() => {
diff --git a/src/components/instance/Subnav.js b/src/components/instance/Subnav.js
index 53b2f3873..121d27e5d 100644
--- a/src/components/instance/Subnav.js
+++ b/src/components/instance/Subnav.js
@@ -9,11 +9,10 @@ import { ErrorBoundary } from 'react-error-boundary';
import appState from '../../functions/state/appState';
import instanceState from '../../functions/state/instanceState';
-import icon from '../../functions/select/icon';
-import routeIcon from '../../functions/select/routeIcon';
import ErrorFallback from '../shared/ErrorFallback';
import addError from '../../functions/api/lms/addError';
import useInstanceAuth from '../../functions/state/instanceAuths';
+import config from '../../config';
function Subnav({ routes = [] }) {
const { compute_stack_id, customer_id } = useParams();
@@ -27,41 +26,49 @@ function Subnav({ routes = [] }) {
(s) => {
const selectedInstance = s.instances && s.instances.find((i) => i.compute_stack_id === compute_stack_id);
const otherInstances = s.instances && s.instances.filter((i) => i.compute_stack_id !== compute_stack_id);
+
return {
options:
otherInstances &&
otherInstances.map((i) => ({
- label: `${i.instance_name} ${
- ['CREATE_IN_PROGRESS', 'UPDATE_IN_PROGRESS', 'CONFIGURING_NETWORK'].includes(i.status) ? `(${i.status.replace(/_/g, ' ').toLowerCase()})` : ''
- }`,
+ label: (
+
+
+ {i.instance_name} {['CREATE_IN_PROGRESS', 'UPDATE_IN_PROGRESS', 'CONFIGURING_NETWORK'].includes(i.status) && `(${i.status.replace(/_/g, ' ').toLowerCase()})`}
+
+ ),
value: i.compute_stack_id,
- is_local: i.is_local,
has_auth: instanceAuths[i.compute_stack_id],
is_unavailable: ['CREATE_IN_PROGRESS', 'UPDATE_IN_PROGRESS', 'CONFIGURING_NETWORK'].includes(i.status),
})),
activeOption: {
- label: selectedInstance?.instance_name,
+ label: (
+
+
+ {selectedInstance?.instance_name}
+
+ ),
value: compute_stack_id,
- is_local: selectedInstance?.is_local,
},
};
},
- [compute_stack_id]
+ [compute_stack_id],
);
- const currentRoute = routes?.find((r) => r.link === location.pathname.split(compute_stack_id)[1].split('/')[1]);
- const activeRoute = {
- label: currentRoute?.label,
- value: currentRoute?.link,
- iconCode: currentRoute?.iconCode,
- };
+ const linkPrefix = `/o/${customer_id}/i/${compute_stack_id}`;
+ const linkRoute = location.pathname.split(`/i/${compute_stack_id}`)[1].split('/')[1];
+ const currentRoute = routes?.find((r) => r.link === linkRoute);
const navigateFn = ({ value, has_auth, is_unavailable }) => {
if (is_unavailable) {
return false;
}
if (has_auth) {
- return navigate(`/o/${customer_id}/i/${value}/${activeRoute.value}`);
+ return navigate(`/o/${customer_id}/i/${value}/${currentRoute?.link}`);
}
return navigate(`/o/${customer_id}/instances/login`);
};
@@ -69,33 +76,31 @@ function Subnav({ routes = [] }) {
return (
addError({ error: { message: error.message, componentStack } })} FallbackComponent={ErrorFallback}>
-
- 'No other instances available'}
- styles={{
- option: (styles, { data }) => ({ ...styles, opacity: data.is_unavailable ? 0.5 : 1, ...icon(data.is_local, data.is_unavailable) }),
- singleValue: (styles, { data }) => ({ ...styles, ...icon(data.is_local, data.is_unavailable) }),
- }}
- />
-
+ {!config.is_local_studio && (
+
+ 'No other instances available'}
+ />
+
+ )}
- {routes.map((route) => (
+ {routes?.map((route) => (
-
+
{route.label || route.link}
{!!alarms && route.link === 'status' && {alarms} }
@@ -107,19 +112,29 @@ function Subnav({ routes = [] }) {
className="react-select-container"
classNamePrefix="react-select"
width="200px"
- onChange={({ value }) => navigate(`/o/${customer_id}/i/${compute_stack_id}/${value}`)}
- options={
- activeRoute.value ? routes.filter((r) => r.link !== activeRoute.value).map((route) => ({ label: route.label, value: route.link, iconCode: route.iconCode })) : null
- }
- isLoading={!activeRoute.value}
- value={activeRoute}
- defaultValue={activeRoute.value}
+ onChange={({ value }) => navigate(`${linkPrefix}/${value}`)}
+ options={routes?.map((route) => ({
+ label: (
+
+
+ {route.label}
+
+ ),
+ value: route.link,
+ }))}
+ isLoading={!currentRoute?.link}
+ value={{
+ label: (
+
+
+ {currentRoute?.label}
+
+ ),
+ value: currentRoute?.link,
+ }}
+ defaultValue={currentRoute?.link}
isSearchable={false}
isClearable={false}
- styles={{
- option: (styles, { data }) => ({ ...styles, ...routeIcon(data.iconCode) }),
- singleValue: (styles, { data }) => ({ ...styles, ...routeIcon(data.iconCode) }),
- }}
/>
diff --git a/src/components/instance/browse/BrowseDatatable.js b/src/components/instance/browse/BrowseDatatable.js
index fd4e70716..2be11e181 100644
--- a/src/components/instance/browse/BrowseDatatable.js
+++ b/src/components/instance/browse/BrowseDatatable.js
@@ -122,7 +122,7 @@ function BrowseDatatable({ tableState, setTableState, activeTable }) {
autoRefresh={tableState.autoRefresh}
refresh={() => setLastUpdate(Date.now())}
toggleAutoRefresh={() => setTableState({ ...tableState, autoRefresh: !tableState.autoRefresh })}
- toggleFilter={() => setTableState({ ...tableState, showFilter: !tableState.showFilter })}
+ toggleFilter={() => setTableState({ ...tableState, showFilter: !tableState.showFilter })}
/>
@@ -137,24 +137,22 @@ function BrowseDatatable({ tableState, setTableState, activeTable }) {
sorted={tableState.sorted.length ? tableState.sorted : [{ id: tableState.hashAttribute, desc: false }]}
loading={loading && !tableState.autoRefresh}
onFilteredChange={(value) => {
- setTableState({ ...tableState, page: 0, filtered: value })
+ 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);
- // 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);
+ 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/BrowseDatatableHeader.js b/src/components/instance/browse/BrowseDatatableHeader.js
index a7011049b..7d7292ef3 100644
--- a/src/components/instance/browse/BrowseDatatableHeader.js
+++ b/src/components/instance/browse/BrowseDatatableHeader.js
@@ -25,7 +25,7 @@ function BrowseDatatableHeader({ totalRecords, loading, loadingFilter, refresh,
-
+
auto
@@ -53,7 +53,7 @@ function BrowseDatatableHeader({ totalRecords, loading, loadingFilter, refresh,
title={`Bulk Upload CSV to ${table}`}
onClick={() => navigate(`/o/${customer_id}/i/${compute_stack_id}/browse/${schema}/${table}/csv`)}
>
-
+
diff --git a/src/components/instance/browse/EntityManager.js b/src/components/instance/browse/EntityManager.js
index c1ce7a8e0..d992880f5 100644
--- a/src/components/instance/browse/EntityManager.js
+++ b/src/components/instance/browse/EntityManager.js
@@ -11,7 +11,7 @@ function EntityManager({ items, activeItem, activeSchema = false, showForm, base
const [isDropping, toggleDropItem] = useState(false);
const [isCreating, toggleCreate] = useState(false);
const registration = useStoreState(instanceState, (s) => s.registration);
- const [ major, minor ] = registration?.version.split('.') || [];
+ const [major, minor] = registration?.version?.split('.') || [];
const versionAsFloat = parseFloat(`${major}.${minor}`);
useEffect(() => {
@@ -63,7 +63,7 @@ function EntityManager({ items, activeItem, activeSchema = false, showForm, base
) : items && !items.length && !showForm ? (
- no visible { versionAsFloat >= 4.2 ? 'databases' : '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 540f8dbe5..bcd1efcf0 100644
--- a/src/components/instance/browse/index.js
+++ b/src/components/instance/browse/index.js
@@ -39,7 +39,11 @@ function NoPrimaryKeyMessage({ table }) {
No Primary Key
- The table { `'${table}'` } does not have a primary key. The HarperDB Studio does not currently support tables without a primary key defined. Please see the HarperDB documention to see the standard HarperDB querying options.
+ The table {`'${table}'`} does not have a primary key. The HarperDB Studio does not currently support tables without a primary key defined. Please see the{' '}
+
+ HarperDB documention
+ {' '}
+ to see the standard HarperDB querying options.
@@ -64,13 +68,15 @@ 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' : `${versionAsFloat >= 4.2 ? 'database' : '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 [hasHashAttr, setHasHashAttr] = useState(true);
const syncInstanceStructure = () => {
buildInstanceStructure({ auth, url });
- }
+ };
const checkForHashAttribute = () => {
async function check() {
@@ -87,7 +93,6 @@ function BrowseIndex() {
const validate = () => {
if (structure) {
-
/*
* FIXME: There is a fair amount of logic scattered throughout this
* page that could be put in a router-level validation function.
@@ -101,56 +106,40 @@ function BrowseIndex() {
const tables = Object.keys(structure?.[schema] || {});
if (!schemas.length && location.pathname !== '/browse') {
- setEntities({ schemas: [], tables: [], activeTable: false });
- navigate(`/o/${customer_id}/i/${compute_stack_id}/browse`);
- return;
+ setEntities({ schemas: [], tables: [], activeTable: false });
+ navigate(`/o/${customer_id}/i/${compute_stack_id}/browse`);
+ return;
}
// redirect to a valid schema if path doesn't match database's schema
if (schemas.length && !schemas.includes(schema)) {
- navigate(`/o/${customer_id}/i/${compute_stack_id}/browse/${schemas[0]}`);
- return;
+ navigate(`/o/${customer_id}/i/${compute_stack_id}/browse/${schemas[0]}`);
+ return;
}
// redirect to a valid table if path doesn't match database's schema
if (tables.length && !tables.includes(table)) {
- navigate(`/o/${customer_id}/i/${compute_stack_id}/browse/${schema}/${tables[0]}`);
- return;
+ navigate(`/o/${customer_id}/i/${compute_stack_id}/browse/${schema}/${tables[0]}`);
+ return;
}
if (entities.activeTable !== `${compute_stack_id}:${schema}:${table}`) {
setTableState(defaultTableState);
}
setEntities({ schemas, tables, activeTable: `${compute_stack_id}:${schema}:${table}` });
-
}
- }
+ };
// eslint-disable-next-line react-hooks/exhaustive-deps
- useEffect(validate, [structure, schema, table, compute_stack_id])
+ useEffect(validate, [structure, schema, table, compute_stack_id]);
useEffect(syncInstanceStructure, [auth, url, schema, table]);
return (
- addError({ error: { message: error.message, componentStack } })}
- FallbackComponent={ErrorFallback}>
- = 4.2 ? 'database' : 'schema'}
- showForm={showForm} />
- {
- schema &&
- }
+ addError({ error: { message: error.message, componentStack } })} FallbackComponent={ErrorFallback}>
+ = 4.2 ? 'database' : 'schema'} showForm={showForm} />
+ {schema && }
= 4.2 ? 'databases' : 'schemas'} and tables`} />
@@ -158,21 +147,18 @@ function BrowseIndex() {
- addError({ error: { message: error.message, componentStack } })}
- FallbackComponent={ErrorFallback}>
- {
- schema && table && action === 'csv' && entities.activeTable ? (
-
- ) : schema && table && action && entities.activeTable ? (
-
- ) : schema && table && entities.activeTable ? (
-
- ) : schema && table && !hasHashAttr ? (
-
- ) :
+ addError({ error: { message: error.message, componentStack } })} FallbackComponent={ErrorFallback}>
+ {schema && table && action === 'csv' && entities.activeTable ? (
+
+ ) : schema && table && action && entities.activeTable ? (
+
+ ) : schema && table && entities.activeTable ? (
+
+ ) : schema && table && !hasHashAttr ? (
+
+ ) : (
} />
- }
+ )}
@@ -184,7 +170,6 @@ export const metadata = {
link: 'browse',
label: 'browse',
icon: 'list',
- iconCode: 'f03a',
};
export default BrowseIndex;
diff --git a/src/components/instance/charts/index.js b/src/components/instance/charts/index.js
index 9a733fc09..ded3cab6e 100644
--- a/src/components/instance/charts/index.js
+++ b/src/components/instance/charts/index.js
@@ -21,7 +21,6 @@ export const metadata = {
link: 'charts',
label: 'charts',
icon: 'chart-line',
- iconCode: 'f201',
};
function DashboardIndex() {
@@ -44,7 +43,7 @@ function DashboardIndex() {
setLastUpdate(Date.now());
}
},
- [alert, auth, compute_stack_id, customer_id]
+ [alert, auth, compute_stack_id, customer_id],
);
useEffect(() => {
diff --git a/src/components/instance/config/Details.js b/src/components/instance/config/Details.js
index 570803ff8..797125dbc 100644
--- a/src/components/instance/config/Details.js
+++ b/src/components/instance/config/Details.js
@@ -5,8 +5,9 @@ import { Card, CardBody, Row, Col } from 'reactstrap';
import instanceState from '../../../functions/state/instanceState';
import ContentContainer from '../../shared/ContentContainer';
import CopyableText from '../../shared/CopyableText';
+import config from '../../../config';
-function Details({ clusterNodeName }) {
+function Details({ clusterNodeName, instanceConfig }) {
const url = useStoreState(instanceState, (s) => s.url);
const auth = useStoreState(instanceState, (s) => s.auth);
const totalPriceStringWithInterval = useStoreState(instanceState, (s) => s.totalPriceStringWithInterval);
@@ -20,9 +21,23 @@ function Details({ clusterNodeName }) {
const authHeader = auth?.user ? `${btoa(`${auth.user}:${auth.pass}`)}` : '...';
const iopsString = is_local ? 'HARDWARE LIMIT' : `${storage?.iops}`;
const formatted_creation_date = creation_date ? new Date(creation_date).toLocaleDateString() : 'N/A';
+ const { hostname } = window.location;
+
+ const urlObject = new URL(url);
+
+ const operationsApiURL = !config.is_local_studio
+ ? `${instanceConfig.operationsApi?.network?.securePort ? 'https://' : 'http://'}${urlObject.hostname}:${
+ instanceConfig.operationsApi?.network?.securePort || instanceConfig.operationsApi?.network?.port
+ }`
+ : `${instanceConfig.operationsApi?.network?.securePort ? 'https://' : 'http://'}${hostname}:${
+ instanceConfig.operationsApi?.network?.securePort || instanceConfig.operationsApi?.network?.port
+ }`;
+ const applicationsApiURL = !config.is_local_studio
+ ? `${instanceConfig.http?.securePort ? 'https://' : 'http://'}${urlObject.hostname}:${instanceConfig.http?.securePort || instanceConfig.http?.port}`
+ : `${instanceConfig.http?.securePort ? 'https://' : 'http://'}${hostname}:${instanceConfig.http?.securePort || instanceConfig.http?.port}`;
const version = useStoreState(instanceState, (s) => s.registration?.version);
- const [ major, minor ] = version?.split('.') || [];
+ const [major, minor] = version?.split('.') || [];
const versionAsFloat = parseFloat(`${major}.${minor}`);
return (
@@ -34,14 +49,14 @@ function Details({ clusterNodeName }) {
-
+
= 4.2 ? 'Applications' : 'Custom Functions'} URL`} className="mb-3">
-
+
@@ -50,51 +65,56 @@ function Details({ clusterNodeName }) {
{clusterNodeName ? : 'clustering not enabled'}
-
-
-
-
-
-
-
-
-
- {formatted_creation_date}
-
-
- {instance_region && (
-
-
- {instance_region}
-
-
- )}
-
-
- {totalPriceStringWithInterval}
-
-
-
-
-
- {compute?.compute_ram_string} {prepaid_compute && '(PREPAID)'}
-
-
-
- {!is_local && (
+ {!config.is_local_studio && (
<>
-
-
+
+
- {storage?.data_volume_size_string} {prepaid_storage && '(PREPAID)'}
+
+
-
- {iopsString}
+
+ {formatted_creation_date}
+
+
+ {instance_region && (
+
+
+ {instance_region}
+
+
+ )}
+
+
+ {totalPriceStringWithInterval}
+
+
+
+
+
+ {compute?.compute_ram_string} {prepaid_compute && '(PREPAID)'}
+
+ {!is_local && (
+ <>
+
+
+
+ {storage?.data_volume_size_string} {prepaid_storage && '(PREPAID)'}
+
+
+
+
+
+ {iopsString}
+
+
+ >
+ )}
>
)}
diff --git a/src/components/instance/config/InstanceConfig.js b/src/components/instance/config/InstanceConfig.js
index af6b9ad78..eaaf933da 100644
--- a/src/components/instance/config/InstanceConfig.js
+++ b/src/components/instance/config/InstanceConfig.js
@@ -11,7 +11,7 @@ function InstanceConfig({ instanceConfig }) {
return (
- ) : is_being_modified ? (
+ ) : !config.is_local_studio && is_being_modified ? (
instance updating. please wait.
diff --git a/src/components/instance/config/index.js b/src/components/instance/config/index.js
index 12f51e0f0..a70502a3e 100644
--- a/src/components/instance/config/index.js
+++ b/src/components/instance/config/index.js
@@ -26,7 +26,6 @@ export const metadata = {
link: 'config',
label: 'config',
icon: 'wrench',
- iconCode: 'f0ad',
};
function ConfigIndex() {
@@ -47,11 +46,11 @@ function ConfigIndex() {
const [clusterNodeName, setClusterNodeName] = useState('');
const unusedCompute = useStoreState(
appState,
- (s) => s.subscriptions[is_local ? 'local_compute' : 'cloud_compute']?.filter((p) => !p.value.compute_subscription_name || p.value.compute_quantity_available) || []
+ (s) => s.subscriptions[is_local ? 'local_compute' : 'cloud_compute']?.filter((p) => !p.value.compute_subscription_name || p.value.compute_quantity_available) || [],
);
const unusedStorage = useStoreState(
appState,
- (s) => s.subscriptions?.cloud_storage?.filter((p) => !p.value.storage_subscription_name || p.value.storage_quantity_available) || []
+ (s) => s.subscriptions?.cloud_storage?.filter((p) => !p.value.storage_subscription_name || p.value.storage_quantity_available) || [],
);
const [showPrepaidCompute, setShowPrepaidCompute] = useState(!!compute_subscription_id);
const [showPrepaidStorage, setShowPrepaidStorage] = useState(!!storage_subscription_id);
@@ -81,7 +80,7 @@ function ConfigIndex() {
addError({ error: { message: error.message, componentStack } })} FallbackComponent={ErrorFallback}>
-
+
{isOrgOwner && theme !== 'akamai' && (
@@ -160,8 +159,8 @@ function ConfigIndex() {
instance config (read only)
-
-
+
+
addError({ error: { message: error.message, componentStack } })} FallbackComponent={ErrorFallback}>
{instanceConfig ? : }
diff --git a/src/components/instance/examples/index.js b/src/components/instance/examples/index.js
index 555a788fa..23712ea81 100644
--- a/src/components/instance/examples/index.js
+++ b/src/components/instance/examples/index.js
@@ -7,10 +7,9 @@ export const metadata = {
link: 'examples',
label: 'example code',
icon: 'code',
- iconCode: 'f121',
-}
+};
function InstanceExamples() {
- return
+ return ;
}
export default InstanceExamples;
diff --git a/src/components/instance/functions/index.js b/src/components/instance/functions/index.js
index 31a4ca24e..e96bfa02b 100644
--- a/src/components/instance/functions/index.js
+++ b/src/components/instance/functions/index.js
@@ -16,13 +16,15 @@ export const metadata = {
link: 'functions',
label: 'functions',
icon: 'project-diagram',
- iconCode: 'f542',
};
function CustomFunctionsIndex() {
const auth = useStoreState(instanceState, (s) => s.auth);
const url = useStoreState(instanceState, (s) => s.url);
const custom_functions = useStoreState(instanceState, (s) => s.custom_functions);
+ const registration = useStoreState(instanceState, (s) => s.registration);
+ const [majorVersion, minorVersion] = (registration?.version || '').split('.') || [];
+ const supportsApplicationsAPI = parseFloat(`${majorVersion}.${minorVersion}`) >= 4.2;
const restarting = useStoreState(instanceState, (s) => s.restarting);
const [showManage, setShowManage] = useState(false);
const [loading, setLoading] = useState(true);
@@ -37,16 +39,16 @@ function CustomFunctionsIndex() {
}, [auth, url, restarting]);
useEffect(() => {
- refreshCustomFunctions();
+ refreshCustomFunctions();
}, [refreshCustomFunctions]);
useEffect(() => {
- const isConfigured = custom_functions?.is_enabled && custom_functions?.port;
+ const isConfigured = (custom_functions?.is_enabled && custom_functions?.port) || supportsApplicationsAPI;
setShowManage(isConfigured);
if (isConfigured) {
setConfiguring(false);
}
- }, [custom_functions]);
+ }, [custom_functions, supportsApplicationsAPI]);
useInterval(() => {
if (configuring) refreshCustomFunctions();
diff --git a/src/components/instance/functions/manage/CodeEditor.js b/src/components/instance/functions/manage/CodeEditor.js
index 4a60edac8..5ad967677 100644
--- a/src/components/instance/functions/manage/CodeEditor.js
+++ b/src/components/instance/functions/manage/CodeEditor.js
@@ -20,7 +20,6 @@ function CodeEditor() {
const [originalCode, setOriginalCode] = useState('');
const setEditorToFile = useCallback(async () => {
-
if (project && type && file && file !== 'undefined') {
const endpoint_code = await getCustomFunction({ auth, url, project, type, file });
setCode(endpoint_code?.message);
@@ -59,7 +58,7 @@ function CodeEditor() {
reload
-
+
|
navigate(`/o/${customer_id}/i/${compute_stack_id}/functions/deploy/${project}`)} color="link" className="me-2">
@@ -72,32 +71,16 @@ function CodeEditor() {
{code ? (
-
+
-
+
-
-
+
diff --git a/src/components/instance/functions/manage/EntityReloader.js b/src/components/instance/functions/manage/EntityReloader.js
index b740b9c7b..e5d3dc162 100644
--- a/src/components/instance/functions/manage/EntityReloader.js
+++ b/src/components/instance/functions/manage/EntityReloader.js
@@ -14,7 +14,7 @@ function EntityReloader({ loading, refreshCustomFunctions, restarting }) {
refresh projects
-
+
|
restartService({ auth, url, service: 'custom_functions' })} className="me-2">
diff --git a/src/components/instance/functions/manage/index.js b/src/components/instance/functions/manage/index.js
index 2d5715f4b..171236d03 100644
--- a/src/components/instance/functions/manage/index.js
+++ b/src/components/instance/functions/manage/index.js
@@ -32,13 +32,10 @@ function getRelativeFilepath(absolutePath) {
// 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];
@@ -46,28 +43,23 @@ 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({
isCurrentInstance: csId === thisCsId,
auth,
- instance
+ instance,
});
-
}
return memo;
-
}, []);
-
}
function ManageIndex({ refreshCustomFunctions, loading }) {
-
const { compute_stack_id } = useParams();
const registration = useStoreState(instanceState, (s) => s.registration);
const { fileTree } = useStoreState(instanceState, (s) => s.custom_functions);
@@ -76,15 +68,13 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
const [majorVersion, minorVersion] = (registration?.version || '').split('.') || [];
const supportsApplicationsAPI = parseFloat(`${majorVersion}.${minorVersion}`) >= 4.2;
const instances = useStoreState(appState, (s) => s.instances);
- const [ instanceAuths ] = useInstanceAuth({});
- const [ editorCache, setEditorCache ] = useEditorCache({});
- const theme = useStoreState(appState, (s) => s.theme);
- const [ restartingInstance, setRestartingInstance ] = useState(false);
+ const [instanceAuths] = useInstanceAuth({});
+ const [editorCache, setEditorCache] = useEditorCache({});
+ const [restartingInstance, setRestartingInstance] = useState(false);
const alert = useAlert();
function removeFileFromLocalStorage({ path }) {
-
- const updatedCache = {...editorCache};
+ const updatedCache = { ...editorCache };
const fileKey = `${compute_stack_id}_${path}`;
if (fileKey in updatedCache) {
@@ -92,7 +82,7 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
}
setEditorCache({
- ...updatedCache
+ ...updatedCache,
});
}
@@ -103,29 +93,25 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
setEditorCache({
...editorCache,
- [fileKey]: content
+ [fileKey]: content,
});
-
}
async function restartWithLoadingState({ auth: instanceAuth, url: instanceUrl }) {
-
setRestartingInstance(true);
- setTimeout(async () => {
+ setTimeout(async () => {
await restartService({
auth: instanceAuth,
url: instanceUrl,
- service: 'http_workers'
+ service: 'http_workers',
});
setRestartingInstance(false);
}, 100);
-
}
// save file to instance
async function saveFileToInstance(selectedFile, restartRequired) {
-
// handle cached situation
// - NOTE: this 'selectedFile' is not reactive.
// - remove cache entry for this file
@@ -137,7 +123,7 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
url,
project: selectedFile.project,
file: filepathRelativeToProjectDir,
- payload: selectedFile.content
+ payload: selectedFile.content,
};
const { error, message } = await setComponentFile(payload);
@@ -154,15 +140,12 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
selectedFile.cached = false;
await refreshCustomFunctions();
-
}
// eslint-disable-next-line no-empty-function
- async function renameFolder() {
- }
+ 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;
@@ -172,7 +155,7 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
auth,
url,
project,
- file: getRelativeFilepath(path)
+ file: getRelativeFilepath(path),
});
await setComponentFile({
@@ -180,15 +163,13 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
url,
project,
file: newFilenameRelativePath,
- payload: content
- })
+ payload: content,
+ });
refreshCustomFunctions();
-
}
async function selectNewFile(selectedFile) {
-
const { path, project, name } = selectedFile;
const newFile = getRelativeFilepath(path);
const fileCacheKey = `${compute_stack_id}_${path}`;
@@ -196,15 +177,13 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
const isCached = fileCacheKey in editorCache;
if (isCached) {
-
return {
cached: isCached,
content: cachedFile,
path,
project,
- name
+ name,
};
-
}
// TODO: set file content to local storage copy if it exists.
@@ -213,11 +192,10 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
auth,
url,
project,
- file: newFile
+ file: newFile,
});
if (error) {
-
alert.error(content);
return {
@@ -225,9 +203,8 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
path,
project,
name,
- cached: false
+ cached: false,
};
-
}
return {
@@ -235,18 +212,16 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
content,
path,
project,
- name
+ name,
};
-
}
async function deleteFile(f) {
-
const { error, message } = await dropComponent({
auth,
url,
project: f.project,
- file: getRelativeFilepath(f.path)
+ file: getRelativeFilepath(f.path),
});
if (error) {
@@ -254,15 +229,13 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
}
await refreshCustomFunctions();
-
}
async function deletePackage({ name: project }) {
-
const { error, message } = await dropComponent({
auth,
url,
- project
+ project,
});
if (error) {
@@ -272,12 +245,9 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
await restartService({ auth, url, service: 'http_workers' });
await refreshCustomFunctions();
-
}
async function deleteFolder({ path, project }) {
-
-
const targetDirpath = getRelativeFilepath(path);
// if we're deleting a top-level directory, that's a project,
@@ -285,45 +255,38 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
// relative to project name as 'file'.
if (targetDirpath.length > 0) {
-
const { error, message } = await dropComponent({
auth,
url,
project,
- file: targetDirpath
+ file: targetDirpath,
});
if (error) {
alert.error(message);
}
-
} else {
-
const { error, message } = await dropComponent({
auth,
url,
- project
+ project,
});
if (error) {
alert.error(message);
}
-
}
await refreshCustomFunctions();
-
}
async function createNewProject(newProjectName) {
-
const { error, message } = await addComponent({
auth,
url,
- project: newProjectName
+ project: newProjectName,
});
-
if (error) {
alert.error(message);
}
@@ -331,11 +294,9 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
await restartService({ auth, url, service: 'http_workers' });
await refreshCustomFunctions();
-
}
async function createNewProjectFolder(newFolderName, parentFolder) {
-
const { path, project } = parentFolder;
const relativeDirpath = getRelativeFilepath(path);
const relativeFilepath = relativeDirpath ? `${relativeDirpath}/${newFolderName}` : newFolderName;
@@ -344,46 +305,40 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
auth,
url,
project,
- file: relativeFilepath
- })
+ file: relativeFilepath,
+ });
if (error) {
alert.error(message);
}
await refreshCustomFunctions();
-
}
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
+ packageUrl,
});
if (error) {
alert.error(message);
}
- // TODO: what do we actually want to do about an invalid package?
+ // TODO: what do we actually want to do about an invalid package?
// change to restartService({ auth, url, service: 'http_workers' });
await restartService({
auth: t.auth,
url: t.instance.url,
- service: 'http_workers'
+ service: 'http_workers',
});
-
});
-
await Promise.all(deployPromises);
await refreshCustomFunctions();
@@ -407,12 +362,9 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
*/
await refreshCustomFunctions();
-
-
}
async function createNewFile(newFilename, parentFolder) {
-
const { path, project } = parentFolder;
const relativeDirpath = getRelativeFilepath(path);
const relativeFilepath = relativeDirpath ? `${relativeDirpath}/${newFilename}` : newFilename;
@@ -426,7 +378,7 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
url,
project,
file: relativeFilepath,
- payload
+ payload,
});
await refreshCustomFunctions();
@@ -436,20 +388,15 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
path: [parentFolder.path, newFilename].join('/'),
project,
};
-
}
async function deployProject({ project, deployTarget }) {
-
- const {
- auth: otherInstanceAuth,
- instance: otherInstance
- } = deployTarget;
+ const { auth: otherInstanceAuth, instance: otherInstance } = deployTarget;
const { payload } = await packageComponent({
auth,
url,
- project: project.name
+ project: project.name,
});
// deploy to targetInstance
@@ -457,29 +404,27 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
auth: otherInstanceAuth,
url: otherInstance.url,
project: project.name,
- payload
- })
+ payload,
+ });
// restart targetInstance
await restartService({
auth: otherInstanceAuth,
url: otherInstance.url,
- service: 'http_workers'
+ service: 'http_workers',
});
// restart this instance
await restartService({
auth,
url,
- service: 'http_workers'
+ service: 'http_workers',
});
await refreshCustomFunctions();
-
}
async function revertFileChanges(selectedFile) {
-
// unset local storage version
removeFileFromLocalStorage({ path: selectedFile.path });
@@ -488,7 +433,7 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
auth,
url,
project: selectedFile.project,
- file: getRelativeFilepath(selectedFile.path)
+ file: getRelativeFilepath(selectedFile.path),
});
if (error) {
@@ -499,16 +444,12 @@ function ManageIndex({ refreshCustomFunctions, loading }) {
// return canonical file content to caller
return message;
-
}
- return supportsApplicationsAPI ?
+ return supportsApplicationsAPI ? (
restartWithLoadingState({auth, url}) }
+ restartInstance={async () => restartWithLoadingState({ auth, url })}
restartingInstance={restartingInstance}
- />
- :
-
-
+ />
+ ) : (
+
+ );
}
export default ManageIndex;
diff --git a/src/components/instance/index.js b/src/components/instance/index.js
index 76c0f16df..cc14e278e 100644
--- a/src/components/instance/index.js
+++ b/src/components/instance/index.js
@@ -41,16 +41,16 @@ function InstanceIndex() {
const [mounted, setMounted] = useState(false);
useAsyncEffect(() => {
- if (auth && customer_id) {
+ if (!config.is_local_studio && auth && customer_id) {
getCustomer({ auth, customer_id });
}
- }, [auth, customer_id]);
+ }, [auth, customer_id, config.is_local_studio]);
useAsyncEffect(() => {
- if (auth && customer_id) {
+ if (!config.is_local_studio && auth && customer_id) {
getAlarms({ auth, customer_id });
}
- }, [auth, customer_id, instances]);
+ }, [auth, customer_id, instances, config.is_local_studio]);
useAsyncEffect(() => {
if (auth && products && regions && subscriptions && customer_id && !instances?.length) {
@@ -103,17 +103,13 @@ function InstanceIndex() {
return (
<>
- {isOrgUser && instances && !loadingInstance ? (
+ {(config.is_local_studio || (isOrgUser && instances)) && !loadingInstance ? (
}>
- {
- hydratedRoutes.map(route => (
-
- ))
- }
-
- } />
+ {hydratedRoutes.map((route) => (
+
+ ))}
+ } />
) : (
diff --git a/src/components/instance/query/Datatable.js b/src/components/instance/query/Datatable.js
index 2c482a0c6..93d4e64c1 100644
--- a/src/components/instance/query/Datatable.js
+++ b/src/components/instance/query/Datatable.js
@@ -7,7 +7,7 @@ import { ErrorBoundary } from 'react-error-boundary';
import config from '../../../config';
import instanceState from '../../../functions/state/instanceState';
-import DataTableHeader from './DatatableHeader';
+import DatatableHeader from './DatatableHeader';
import ChartModal from './ChartModal';
import getQueryData from '../../../functions/instance/getQueryData';
import EmptyPrompt from './EmptyPrompt';
@@ -97,7 +97,7 @@ function Datatable({ query }) {
) : tableState.tableData?.length ? (
addError({ error: { message: error.message, componentStack } })} FallbackComponent={ErrorFallback}>
-
-
- {commaNumbers(totalRecords)} record
- {totalRecords !== 1 ? 's' : ''}
-
-
-
- setShowChartModal(true)}>
-
- create chart
-
- |
- setLastUpdate(Date.now())}>
-
-
- setAutoRefresh(!autoRefresh)}>
- auto
-
-
- |
- toggleFilter({ filtered: showFilter ? [] : filtered, page: 0, showFilter: !showFilter })}>
-
-
-
-
+ return (
+
+
+ {commaNumbers(totalRecords)} record
+ {totalRecords !== 1 ? 's' : ''}
+
+
+
+ {!config.is_local_studio && (
+ <>
+ setShowChartModal(true)}>
+
+ create chart
+
+ |
+ >
+ )}
+ setLastUpdate(Date.now())}>
+
+
+ setAutoRefresh(!autoRefresh)}>
+ auto
+
+
+ |
+ toggleFilter({ filtered: showFilter ? [] : filtered, page: 0, showFilter: !showFilter })}>
+
+
+
+
+ );
}
export default DatatableHeader;
diff --git a/src/components/instance/query/index.js b/src/components/instance/query/index.js
index 32c8e5bee..69312d133 100644
--- a/src/components/instance/query/index.js
+++ b/src/components/instance/query/index.js
@@ -10,7 +10,6 @@ export const metadata = {
link: 'query',
label: 'query',
icon: 'search',
- iconCode: 'f002',
};
function QueryIndex() {
diff --git a/src/components/instance/replication/index.js b/src/components/instance/replication/index.js
index be4db2273..b38270eaa 100644
--- a/src/components/instance/replication/index.js
+++ b/src/components/instance/replication/index.js
@@ -16,7 +16,6 @@ export const metadata = {
link: 'replication',
label: 'replication',
icon: 'cubes',
- iconCode: 'f1e0',
};
function ClusteringIndex() {
diff --git a/src/components/instance/replication/manage/Datatable.js b/src/components/instance/replication/manage/Datatable.js
index 8d88ba236..6a6313a8d 100644
--- a/src/components/instance/replication/manage/Datatable.js
+++ b/src/components/instance/replication/manage/Datatable.js
@@ -41,7 +41,7 @@ function Datatable({ refreshNetwork, loading, setLoading }) {
refresh network
-
+
diff --git a/src/components/instance/replication/setup/ClusterField.js b/src/components/instance/replication/setup/ClusterField.js
index cd826be1d..c7ee8dbdb 100644
--- a/src/components/instance/replication/setup/ClusterField.js
+++ b/src/components/instance/replication/setup/ClusterField.js
@@ -1,42 +1,49 @@
import React, { useState } from 'react';
-import { Row, Col, Input } from 'reactstrap';
+import { Input } from 'reactstrap';
import cn from 'classnames';
-import FormValidationError from '../../../shared/FormValidationError';
-function ClusterField({ label, value, type = 'text', max = null, min = null, editable = false, handleChange, validator = () => true, errorMessage = null, showDivider = true }) {
+function ClusterField({
+ label,
+ value,
+ type = 'text',
+ max = null,
+ min = null,
+ editable = false,
+ handleChange,
+ validator = () => true,
+ errorMessage = 'is required',
+ addSpace = true,
+ valid = false,
+}) {
const [error, setError] = useState(null);
return editable ? (
<>
- {showDivider && }
- {`${label}`}
+ {addSpace && }
+
+ {label} {error}
+
handleChange(e.target.value)}
onBlur={(e) => {
const isValid = validator(e.target.value);
setError(isValid ? null : errorMessage);
}}
/>
- {error && }
>
) : (
-
-
-
-
-
- {`${label}: ${value}`}
-
-
-
-
-
+ <>
+ {addSpace && }
+ {label}
+
+ >
);
}
diff --git a/src/components/instance/replication/setup/ClusterForm.js b/src/components/instance/replication/setup/ClusterForm.js
index e6c409400..2f473a464 100644
--- a/src/components/instance/replication/setup/ClusterForm.js
+++ b/src/components/instance/replication/setup/ClusterForm.js
@@ -10,6 +10,7 @@ import addUser from '../../../../functions/api/instance/addUser';
import FormValidationError from '../../../shared/FormValidationError';
import ClusterField from './ClusterField';
+import addRole from '../../../../functions/api/instance/addRole';
function ClusterForm({ setConfiguring, clusterStatus, refreshStatus, compute_stack_id }) {
const auth = useStoreState(instanceState, (s) => s.auth);
@@ -19,7 +20,7 @@ function ClusterForm({ setConfiguring, clusterStatus, refreshStatus, compute_sta
const clusterPortMax = 65535;
const [formData, setFormData] = useState({
- clusterRole: clusterStatus?.cluster_role?.role || '', // implies that it might not exist, but it should.
+ clusterRole: clusterStatus?.cluster_role?.role,
clusterUsername: clusterStatus?.cluster_user?.username || 'cluster_user',
clusterPassword: clusterStatus?.cluster_user?.password || '',
clusterPort: clusterStatus?.config_cluster_port || '',
@@ -32,7 +33,7 @@ function ClusterForm({ setConfiguring, clusterStatus, refreshStatus, compute_sta
return (
clusterRole?.length > 0 &&
clusterUsername.length > 0 &&
- clusterPassword.length > 0 &&
+ (clusterStatus?.cluster_user?.username || clusterPassword.length > 0) &&
clusterNodeName.length > 0 &&
clusterPort >= clusterPortMin &&
clusterPort <= clusterPortMax
@@ -46,6 +47,22 @@ function ClusterForm({ setConfiguring, clusterStatus, refreshStatus, compute_sta
async function enableClustering() {
setServerResponseError(null);
+ // creates cluster role
+ if (!clusterStatus.cluster_role?.role) {
+ const response = await addRole({
+ auth,
+ url,
+ role: formData.clusterRole,
+ permission: {
+ cluster_user: true,
+ },
+ });
+
+ if (response.error) {
+ return setServerResponseError(response.message);
+ }
+ }
+
// creates cluster user
if (!clusterStatus.cluster_user) {
const response = await addUser({
@@ -57,8 +74,7 @@ function ClusterForm({ setConfiguring, clusterStatus, refreshStatus, compute_sta
});
if (response.error) {
- setServerResponseError(response.message);
- return;
+ return setServerResponseError(response.message);
}
}
@@ -86,8 +102,7 @@ function ClusterForm({ setConfiguring, clusterStatus, refreshStatus, compute_sta
});
if (result.error) {
- setServerResponseError(result.message);
- return;
+ return setServerResponseError(result.message);
}
if (window._kmq) {
@@ -97,30 +112,38 @@ function ClusterForm({ setConfiguring, clusterStatus, refreshStatus, compute_sta
await restartInstance({ auth, url });
setTimeout(() => setConfiguring(true), 0);
- refreshStatus();
+ return refreshStatus();
}
return (
<>
-
+ updateForm({ clusterRole })}
+ value={formData.clusterRole}
+ valid={formData.clusterRole.trim().length > 0}
+ validator={(value) => value.trim().length > 0}
+ editable={!clusterStatus?.cluster_role?.role}
+ addSpace={false}
+ />
updateForm({ clusterUsername })}
value={formData.clusterUsername}
+ valid={formData.clusterUsername.trim().length > 0}
validator={(value) => value.trim().length > 0}
- errorMessage="Please enter a cluster username"
- editable
+ editable={!clusterStatus?.cluster_user?.username}
/>
updateForm({ clusterPassword })}
- value={formData.clusterPassword}
+ value={clusterStatus?.cluster_user?.username ? '********' : formData.clusterPassword}
+ valid={clusterStatus?.cluster_user?.username || formData.clusterPassword.trim().length > 0}
validator={(value) => value.trim().length > 0}
- errorMessage="Please enter a password"
- editable
+ editable={!clusterStatus?.cluster_user?.username}
showDivider={false}
/>
@@ -131,25 +154,27 @@ function ClusterForm({ setConfiguring, clusterStatus, refreshStatus, compute_sta
min={clusterPortMin}
handleChange={(newClusterPort) => updateForm({ clusterPort: newClusterPort })}
value={formData.clusterPort}
+ valid={formData.clusterPort >= clusterPortMin && formData.clusterPort <= clusterPortMax}
validator={(value) => value >= clusterPortMin && value <= clusterPortMax}
- errorMessage={`Please enter a port number between ${clusterPortMin} and ${clusterPortMax}`}
- editable
+ errorMessage={`must be between ${clusterPortMin} and ${clusterPortMax}`}
+ editable={!clusterStatus?.config_cluster_port}
/>
updateForm({ clusterNodeName: newClusterNodeName })}
value={formData.clusterNodeName}
+ valid={formData.clusterNodeName.length > 0}
validator={(value) => value.length > 0}
- errorMessage="Please enter a cluster node name"
- editable
+ editable={!clusterStatus?.node_name}
/>
- {serverResponseError && }
- enableClustering()}>
+
Enable Clustering
+
+ {serverResponseError && }
>
);
}
diff --git a/src/components/instance/roles/JsonViewer.js b/src/components/instance/roles/JsonViewer.js
index 9bf339574..c9f923d9f 100644
--- a/src/components/instance/roles/JsonViewer.js
+++ b/src/components/instance/roles/JsonViewer.js
@@ -22,7 +22,7 @@ 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 [major, minor] = version?.split('.') || [];
const versionAsFloat = parseFloat(`${major}.${minor}`);
const lastUpdate = useStoreState(instanceState, (s) => s.lastUpdate);
const auth = useStoreState(instanceState, (s) => s.auth);
@@ -36,17 +36,19 @@ function JsonViewer({ showAttributes, fetchRoles }) {
const [changed, setChanged] = useState(false);
const [validJSON, setValidJSON] = useState(true);
const hasStructureUser = major >= 3 && (major > 3 || minor >= 3);
- const jsonHeight = hasStructureUser ? "calc(100vh - 440px)" : "calc(100vh - 340px)"
+ const jsonHeight = hasStructureUser ? 'calc(100vh - 440px)' : 'calc(100vh - 340px)';
const icons = {
- checked: manage { versionAsFloat >= 4.2 ? 'databases' : 'schemas' }/tables
,
- unchecked: manage { versionAsFloat >= 4.2 ? 'databases' : 'schemas' }/tables
,
+ checked: manage {versionAsFloat >= 4.2 ? 'databases' : 'schemas'}/tables
,
+ unchecked: manage {versionAsFloat >= 4.2 ? 'databases' : 'schemas'}/tables
,
};
useAsyncEffect(async () => {
- if (role_id && roles) {
- const currentRolePermissions = roles.find((r) => r.id === role_id).permission;
- const defaultStructurePermissions = Array.isArray(currentRolePermissions.structure_user) ? currentRolePermissions.structure_user.join(', ') : currentRolePermissions.structure_user || false;
+ if (role_id && roles && roles.find((r) => r.id === role_id)?.permission) {
+ const currentRolePermissions = roles.find((r) => r.id === role_id)?.permission;
+ const defaultStructurePermissions = Array.isArray(currentRolePermissions.structure_user)
+ ? currentRolePermissions.structure_user.join(', ')
+ : currentRolePermissions.structure_user || false;
const defaultActivePermissions = await buildPermissionStructure({ auth, url, version, currentRolePermissions, showAttributes });
setActivePermissions(defaultActivePermissions);
setNewPermissions(defaultActivePermissions);
@@ -69,7 +71,7 @@ function JsonViewer({ showAttributes, fetchRoles }) {
const permission = { super_user: false, ...newPermissions };
if (hasStructureUser) {
- permission.structure_user = newStructureUser === true ? true : newStructureUser ? newStructureUser.split(/[ ,]+/).filter((v) => v!=='') : false;
+ permission.structure_user = newStructureUser === true ? true : newStructureUser ? newStructureUser.split(/[ ,]+/).filter((v) => v !== '') : false;
}
const response = await alterRole({ permission, id: role_id, auth, url });
@@ -114,8 +116,8 @@ function JsonViewer({ showAttributes, fetchRoles }) {
}}
type="text"
id="permitted_schemas"
- title={`permitted ${ versionAsFloat >= 4.2 ? 'databases' : 'schemas' }`}
- placeholder={`comma-separated ${ versionAsFloat >= 4.2 ? 'databases' : '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/RoleManagerForm.js b/src/components/instance/roles/RoleManagerForm.js
index db4c30e81..311edc7d4 100644
--- a/src/components/instance/roles/RoleManagerForm.js
+++ b/src/components/instance/roles/RoleManagerForm.js
@@ -38,6 +38,8 @@ function RoleManagerForm({ itemType, toggleDropItem, toggleCreate, baseUrl }) {
return alert.error('Role names must have only letters and underscores');
}
+ const permission = itemType === 'cluster user' ? { cluster_user: true } : itemType === 'super user' ? { super_user: true } : {};
+
const response = await addRole({
auth,
url,
@@ -45,10 +47,7 @@ function RoleManagerForm({ itemType, toggleDropItem, toggleCreate, baseUrl }) {
compute_stack_id,
customer_id,
role: entity.name,
- permission: {
- cluster_user: itemType === 'cluster user',
- super_user: itemType === 'super user',
- },
+ permission,
});
if (response.error) {
@@ -62,7 +61,7 @@ function RoleManagerForm({ itemType, toggleDropItem, toggleCreate, baseUrl }) {
};
useEffect(() => {
- toggleDropItem()
+ toggleDropItem();
}, [toggleDropItem]);
return (
diff --git a/src/components/instance/roles/index.js b/src/components/instance/roles/index.js
index eae8e9cef..3f2e754fa 100644
--- a/src/components/instance/roles/index.js
+++ b/src/components/instance/roles/index.js
@@ -15,7 +15,6 @@ export const metadata = {
link: 'roles',
label: 'roles',
icon: 'check-square',
- iconCode: 'f14a',
};
const JSONViewer = lazy(() => import(/* webpackChunkName: "roles-jsonviewer" */ './JsonViewer'));
@@ -39,7 +38,7 @@ function RolesIndex() {
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 [major, minor] = version?.split('.') || [];
const versionAsFloat = parseFloat(`${major}.${minor}`);
const fetchRoles = useCallback(async () => {
@@ -81,9 +80,7 @@ function RolesIndex() {
- {formState.canEdit && (
- edit role > {formState.roleName}
- )}
+ {formState.canEdit && edit role > {formState.roleName}}
{formState.canEdit && (
<>
@@ -96,7 +93,7 @@ function RolesIndex() {
)}
refresh roles
-
+
@@ -112,7 +109,7 @@ function RolesIndex() {
{role_id ? (
- Super Users and Cluster Users have full access to all { versionAsFloat >= 4.2 ? 'databases' : '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 9fa104726..2117c5da4 100644
--- a/src/components/instance/routes.js
+++ b/src/components/instance/routes.js
@@ -1,6 +1,7 @@
import { lazy, React } from 'react';
import Browse from './browse';
+import config from '../../config';
const Charts = lazy(() => import(/* webpackChunkName: "instance-charts" */ './charts'));
const Query = lazy(() => import(/* webpackChunkName: "instance-query" */ './query'));
@@ -11,118 +12,111 @@ const Users = lazy(() => import(/* webpackChunkName: "instance-users" */ './user
const Roles = lazy(() => import(/* webpackChunkName: "instance-roles" */ './roles'));
const Functions = lazy(() => import(/* webpackChunkName: "custom-functions" */ './functions'));
-const browse = {
- element: ,
- path: `browse/:schema?/:table?/:action?/:hash?`,
- link: 'browse',
- label: 'browse',
- icon: 'list',
- iconCode: 'f03a',
-};
-
-const query = {
- element: ,
- path: `query`,
- link: 'query',
- label: 'query',
- icon: 'search',
- iconCode: 'f002',
-};
-
-const users = {
- element: ,
- path: `users/:username?`,
- link: 'users',
- label: 'users',
- icon: 'users',
- iconCode: 'f0c0',
-};
-
-const roles = {
- element: ,
- path: `roles/:role_id?`,
- link: 'roles',
- label: 'roles',
- icon: 'check-square',
- iconCode: 'f14a',
-};
-
-const charts = {
- element: ,
- path: `charts`,
- link: 'charts',
- label: 'charts',
- icon: 'chart-line',
- iconCode: 'f201',
-};
-
-const cluster = {
- element: ,
- path: `replication`,
- link: 'replication',
- label: 'replication',
- icon: 'cubes',
- iconCode: 'f1e0',
-};
-
-const functions = {
- element: ,
- path: `functions/:action?/:project?/:type?/:file?`,
- link: 'functions',
- label: 'functions',
- icon: 'project-diagram',
- iconCode: 'f542',
-};
-
-const applications = {
- element: ,
- path: 'applications',
- link: 'applications',
- label: 'applications',
- icon: 'project-diagram',
- iconCode: 'f542',
-};
-
-const metrics = {
- element: ,
- path: `status`,
- link: 'status',
- label: 'status',
- icon: 'tachometer',
- iconCode: 'f0e4',
-};
-
-const config = {
- element: ,
- path: `config`,
- link: 'config',
- label: 'config',
- icon: 'wrench',
- iconCode: 'f0ad',
-};
-
-
-const routes = ({ super_user, version=null }) => {
+const routes = ({ super_user, version = null }) => {
+ const browse = {
+ element: ,
+ path: `browse/:schema?/:table?/:action?/:hash?`,
+ link: 'browse',
+ label: 'browse',
+ icon: 'list',
+ };
+
+ const query = {
+ element: ,
+ path: `query`,
+ link: 'query',
+ label: 'query',
+ icon: 'search',
+ };
+
+ const users = {
+ element: ,
+ path: `users/:username?`,
+ link: 'users',
+ label: 'users',
+ icon: 'users',
+ };
+
+ const roles = {
+ element: ,
+ path: `roles/:role_id?`,
+ link: 'roles',
+ label: 'roles',
+ icon: 'check-square',
+ };
+
+ const charts = {
+ element: ,
+ path: `charts`,
+ link: 'charts',
+ label: 'charts',
+ icon: 'chart-line',
+ };
+
+ const cluster = {
+ element: ,
+ path: `replication`,
+ link: 'replication',
+ label: 'replication',
+ icon: 'cubes',
+ };
+
+ const functions = {
+ element: ,
+ path: `functions/:action?/:project?/:type?/:file?`,
+ link: 'functions',
+ label: 'functions',
+ icon: 'project-diagram',
+ };
+
+ const applications = {
+ element: ,
+ path: 'applications',
+ link: 'applications',
+ label: 'applications',
+ icon: 'project-diagram',
+ };
+
+ const metrics = {
+ element: ,
+ path: `status`,
+ link: 'status',
+ label: 'status',
+ icon: 'tachometer-alt',
+ };
+
+ const configure = {
+ element: ,
+ path: `config`,
+ link: 'config',
+ label: 'config',
+ icon: 'wrench',
+ };
let supportsApplications = false;
if (version) {
-
- const [ a, b ] = version?.split('.') || [];
+ 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 (config.is_local_studio && super_user) {
+ return [browse, query, users, roles, cluster, supportsApplications ? applications : functions, metrics, configure];
+ }
+ if (super_user) {
+ return [browse, query, users, roles, charts, cluster, supportsApplications ? applications : functions, metrics, configure];
}
- if (super_user) {
- return [browse, query, users, roles, charts, cluster, supportsApplications ? applications : functions, metrics, config];
- }
+ if (config.is_local_studio) {
+ return [browse, query, charts];
+ }
- return [browse, query, charts];
-
-}
+ return [browse, query];
+};
export default routes;
diff --git a/src/components/instance/status/Alarms.js b/src/components/instance/status/Alarms.js
index 2732742fb..8208afc6c 100644
--- a/src/components/instance/status/Alarms.js
+++ b/src/components/instance/status/Alarms.js
@@ -51,10 +51,9 @@ function Alarms() {
addError({ error: { message: error.message, componentStack } })} FallbackComponent={ErrorFallback}>
alarms
-
-
+
setLastUpdate(Date.now())}>
-
+
setAutoRefresh(!autoRefresh)}>
auto
@@ -64,11 +63,11 @@ function Alarms() {
-
-
+
+
status
-
+
date
@@ -89,6 +88,7 @@ function Alarms() {
);
}
diff --git a/src/components/instance/status/Jobs.js b/src/components/instance/status/Jobs.js
index 2d095b627..fd1e8ea30 100644
--- a/src/components/instance/status/Jobs.js
+++ b/src/components/instance/status/Jobs.js
@@ -60,10 +60,9 @@ function Jobs() {