Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "harperdb-studio",
"version": "4.4.2",
"version": "4.5.0",
"description": "A UI for HarperDB",
"deploymentUrl": "studio.harperdb.io",
"private": true,
Expand Down
187 changes: 16 additions & 171 deletions src/components/App.js
Original file line number Diff line number Diff line change
@@ -1,178 +1,23 @@
import React, { useEffect, useState, lazy, Suspense } from 'react';
import React from 'react';

import { Route, Routes, Navigate, useNavigate, useLocation, Outlet, useParams } from 'react-router-dom';
import ReactGA from 'react-ga4';
import { BrowserRouter } from 'react-router-dom';
import { Elements } from '@stripe/react-stripe-js';
import { positions, Provider } from 'react-alert';

import { useStoreState } from 'pullstate';
import useInterval from 'use-interval';
import { positions, useAlert } from 'react-alert';
import { ErrorBoundary } from 'react-error-boundary';
import queryString from 'query-string';

import appState from '../functions/state/appState';
import usePersistedUser from '../functions/state/persistedUser';
import config from '../config';

import Loader from './shared/Loader';
import Maintenance from './shared/Maintenance';
import ErrorFallback from './shared/ErrorFallback';
import ErrorFallbackAuth from './shared/ErrorFallbackAuth';

import getProducts from '../functions/api/lms/getProducts';
import getRegions from '../functions/api/lms/getRegions';
import getWavelengthRegions from '../functions/api/lms/getWavelengthRegions';
import getCurrentVersion from '../functions/api/lms/getCurrentVersion';
import checkVersion from '../functions/app/checkVersion';
import init from '../functions/app/init';
import refreshUser from '../functions/app/refreshUser';
import changeFavIcon from '../functions/app/changeFavIcon';
import getAkamaiRegions from '../functions/api/lms/getAkamaiRegions';

const TopNav = lazy(() => import(/* webpackChunkName: "topnav" */ './TopNav'));
const SignUp = lazy(() => import(/* webpackChunkName: "signUp" */ './auth/SignUp'));
const SignIn = lazy(() => import(/* webpackChunkName: "signIn" */ './auth/SignIn'));
const ResetPassword = lazy(() => import(/* webpackChunkName: "resetPassword" */ './auth/ResetPassword'));
const UpdatePassword = lazy(() => import(/* webpackChunkName: "updatePassword" */ './auth/UpdatePassword'));
const Organization = lazy(() => import(/* webpackChunkName: "organization" */ './organization'));
const Organizations = lazy(() => import(/* webpackChunkName: "organizations" */ './organizations'));
const Instances = lazy(() => import(/* webpackChunkName: "instances" */ './instances'));
const Instance = lazy(() => import(/* webpackChunkName: "instance" */ './instance'));
const Profile = lazy(() => import(/* webpackChunkName: "profile" */ './profile'));

const versionAlertOptions = { timeout: 0, position: positions.BOTTOM_CENTER };
let controller;

function ValidatedRoute(auth) {
// ensure org id is valid for user. if not, redirect to organizations
const { customer_id } = useParams();

if (auth.auth.orgs.some((o) => o.customer_id === customer_id)) {
return <Outlet />;
}
return <Navigate to="/organization" replace />;
}

function App() {
const canonicalUrl = document.querySelector('link[rel="canonical"]');
const navigate = useNavigate();
const alert = useAlert();
const location = useLocation();
const { search, pathname } = location;
const { redirect } = queryString.parse(search);
const auth = useStoreState(appState, (s) => s.auth);
const theme = useStoreState(appState, (s) => s.theme);
const products = useStoreState(appState, (s) => s.products);
const regions = useStoreState(appState, (s) => s.regions);
const wavelengthRegions = useStoreState(appState, (s) => s.wavelengthRegions);
const akamaiRegions = useStoreState(appState, (s) => s.akamaiRegions);
const version = useStoreState(appState, (s) => s.version);
const [fetchingUser, setFetchingUser] = useState(true);
const [showVersionAlert, setShowVersionAlert] = useState(false);
const [persistedUser, setPersistedUser] = usePersistedUser({});
const loggedIn = auth?.user_id;
const isNotEmployee = loggedIn && auth?.email.indexOf('harperdb.io') === -1 && auth?.email.indexOf('deliciousmonster.com') === -1;
const isMaintenance = version?.maintenance && isNotEmployee;

ReactGA.initialize(config.google_analytics_code);

useEffect(() => {
ReactGA.send({ hitType: 'pageview', page: window.location.pathname + window.location.search });
}, [location.pathname]);

useEffect(() => {
changeFavIcon(persistedUser?.theme);
}, [persistedUser?.theme]);

useEffect(() => {
setShowVersionAlert(checkVersion({ apiVersion: version.studio }));
}, [version.studio]);

useEffect(() => {
if (auth?.email) {
if (window._kmq) {
window._kmq.push(['identify', auth.email]);
}
if (auth?.update_password) {
navigate('/update-password');
}
if (redirect) {
navigate(redirect);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [auth?.email]);

useEffect(() => {
if (showVersionAlert) alert.info(`HarperDB Studio v${showVersionAlert} is now available. Refresh to update.`, versionAlertOptions);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showVersionAlert]);

useEffect(() => {
canonicalUrl.href = window.location.href;
}, [location, canonicalUrl]);

useEffect(() => {
init({ currentPath: location.pathname, navigate, persistedUser, setPersistedUser, setFetchingUser, controller });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useInterval(() => {
if (!products) getProducts();
if (!regions) getRegions();
if (!wavelengthRegions) getWavelengthRegions();
if (!akamaiRegions) getAkamaiRegions();
}, config.refresh_content_interval);

useInterval(() => {
getCurrentVersion();
refreshUser({ auth, controller });
}, config.check_version_interval);
import stripePromise from '../functions/stripe/stripePromise';
import AlertTemplate from './shared/Alert';
import App from './layouts/App';

function Template() {
return (
<div className={`${theme} ${config.maintenance ? 'maintenance' : ''}`}>
<div id="app-container">
<Suspense fallback={<Loader header=" " spinner />}>
<TopNav isMaintenance={isMaintenance} loggedIn={loggedIn} />
</Suspense>
{fetchingUser ? (
<Loader header="signing in" spinner />
) : loggedIn ? (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<Loader header=" " spinner />}>
{/* can we put instance routes in here, each in a suspense tag (since they're lazily loaded) */}
<Routes>
<Route element={isMaintenance ? <Maintenance /> : <UpdatePassword />} path="/update-password" />
<Route element={isMaintenance ? <Maintenance /> : <Profile />} path="/profile/*" />
<Route element={isMaintenance ? <Maintenance /> : <ValidatedRoute auth={auth} />}>
<Route element={<Instance />} path="/o/:customer_id/i/:compute_stack_id/*" />
<Route element={<Instances />} path="/o/:customer_id/instances/:action?/:purchaseStep?" />
<Route element={<Organization />} path="/o/:customer_id/*" />
</Route>
<Route element={isMaintenance ? <Maintenance /> : <Organizations />} path="/:list?/:action?" />
<Route element={<Navigate to="/" replace />} />
</Routes>
</Suspense>
</ErrorBoundary>
) : (
<ErrorBoundary FallbackComponent={ErrorFallbackAuth}>
<Suspense fallback={<Loader header=" " spinner />}>
<Routes>
<Route element={<SignIn />} path="/" />
<Route element={config.maintenance ? <Maintenance /> : <SignUp />} path="/sign-up" />
<Route element={isMaintenance ? <Maintenance /> : <ResetPassword />} path="/reset-password" />
<Route path="*" element={<Navigate to={`/?redirect=${pathname}${search}`} replace />} />
</Routes>
</Suspense>
</ErrorBoundary>
)}
</div>
<div id="app-bg-color" />
<div className="version">
HarperDB Studio v{config.studio_version}
{version.maintenance && ' - MAINTENANCE MODE'}
</div>
</div>
<Elements stripe={stripePromise}>
<Provider template={AlertTemplate} timeout={4000} position={positions.TOP_CENTER}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</Elements>
);
}

export default App;
export default Template;
78 changes: 12 additions & 66 deletions src/components/LocalApp.js
Original file line number Diff line number Diff line change
@@ -1,72 +1,18 @@
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 React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { positions, Provider } from 'react-alert';

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
}, []);
import AlertTemplate from './shared/Alert';
import App from './layouts/LocalApp';

function Template() {
return (
<div id="local-studio" className={`${persistedUser.theme}`}>
<div id="app-container">
<Suspense fallback={<Loader header=" " spinner />}>
<TopNav loggedIn={instanceAuths?.local?.valid} />
</Suspense>
{fetchingUser ? (
<Loader header="signing in" spinner />
) : instanceAuths?.local?.valid ? (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<Loader header=" " spinner />}>
{/* can we put instance routes in here, each in a suspense tag (since they're lazily loaded) */}
<Routes>
<Route element={<Instance />} path="/o/:customer_id/i/:compute_stack_id/*" />
<Route element={<Navigate to="/" replace />} />
</Routes>
</Suspense>
</ErrorBoundary>
) : (
<ErrorBoundary FallbackComponent={ErrorFallbackAuth}>
<Suspense fallback={<Loader header=" " spinner />}>
<Routes>
<Route element={<SignIn />} path="/" />
</Routes>
</Suspense>
</ErrorBoundary>
)}
</div>
<div id="app-bg-color" />
<div className="version">HarperDB Local Studio v{config.studio_version}</div>
</div>
<Provider template={AlertTemplate} timeout={4000} position={positions.TOP_CENTER}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
);
}

export default LocalApp;
export default Template;
1 change: 0 additions & 1 deletion src/components/instance/config/Details.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ function Details({ clusterNodeName, instanceConfig }) {
const iopsString = is_local ? 'HARDWARE LIMIT' : `${storage?.iops}`;
const formatted_creation_date = creation_date ? new Date(creation_date).toLocaleDateString() : 'N/A';
const { hostname, origin } = window.location;

const urlObject = new URL(config.is_local_studio ? origin : url);

const operationsApiURL = !config.is_local_studio
Expand Down
9 changes: 7 additions & 2 deletions src/components/instance/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,12 @@ function InstanceIndex() {
await registrationInfo({ auth: instanceAuth, url: thisInstance.url });
setLoadingInstance(false);
if (error) {
setTimeout(() => navigate(`/o/${customer_id}/instances`), 10);
if (config.is_local_studio) {
setInstanceAuths({ ...instanceAuths, local: false });
setTimeout(() => navigate(`/`), 10);
} else {
setTimeout(() => navigate(`/o/${customer_id}/instances`), 10);
}
}
}
}, [thisInstance]);
Expand All @@ -85,7 +90,7 @@ function InstanceIndex() {
);

useInterval(async () => {
if (url) {
if (url && !config.is_local_studio) {
const result = await userInfo({ auth: instanceAuth, url });
if (result.error && result.message !== 'Network request failed' && !restarting) {
alert.error('Unable to connect to instance.');
Expand Down
11 changes: 7 additions & 4 deletions src/components/instance/status/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import SystemInfo from './SystemInfo';
import Logs from './Logs';
import Jobs from './Jobs';
import Alarms from './Alarms';
import config from '../../../config';

function MetricsIndex() {
return (
Expand All @@ -15,12 +16,14 @@ function MetricsIndex() {
<Col lg="6" xs="12">
<Logs />
</Col>
<Col lg="3" xs="12">
<Col lg={config.is_local_studio ? 6 : 3} xs="12">
<Jobs />
</Col>
<Col lg="3" xs="12">
<Alarms />
</Col>
{!config.is_local_studio && (
<Col lg="3" xs="12">
<Alarms />
</Col>
)}
</Row>
);
}
Expand Down
Loading